From 96f1f522b9a47166bad78ea4b940ecd808eee3b5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 20 Aug 2020 14:38:33 +0200 Subject: [PATCH 01/68] core-api: add DiscoveryApi + UrlPatternDiscovery implementation --- .../src/apis/definitions/DiscoveryApi.ts | 47 +++++++++++ .../core-api/src/apis/definitions/index.ts | 1 + .../DiscoveryApi/UrlPatternDiscovery.test.ts | 84 +++++++++++++++++++ .../DiscoveryApi/UrlPatternDiscovery.ts | 58 +++++++++++++ .../implementations/DiscoveryApi/index.ts | 21 +++++ .../src/apis/implementations/index.ts | 1 + 6 files changed, 212 insertions(+) create mode 100644 packages/core-api/src/apis/definitions/DiscoveryApi.ts create mode 100644 packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts create mode 100644 packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts create mode 100644 packages/core-api/src/apis/implementations/DiscoveryApi/index.ts diff --git a/packages/core-api/src/apis/definitions/DiscoveryApi.ts b/packages/core-api/src/apis/definitions/DiscoveryApi.ts new file mode 100644 index 0000000000..b0773086c7 --- /dev/null +++ b/packages/core-api/src/apis/definitions/DiscoveryApi.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createApiRef } from '../ApiRef'; + +/** + * The discovery API is used to provide a mechanism for plugins to + * discover the endpoint to use to talk to their backend counterpart. + * + * The purpose of the discovery API is to allow for many different deployment + * setups and routing methods through a central configuration, instead + * of letting each individual plugin manage that configuration. + * + * Implementations of the discovery API can be a simple as a URL pattern + * using the pluginId, but could also have overrides for individual plugins, + * or query a separate discovery service. + */ +export type DiscoveryApi = { + /** + * Returns the HTTP base backend URL for a given plugin, without a trailing slash. + * + * This method must always be called just before making a request. as opposed to + * fetching the URL when constructing an API client. That is to ensure that more + * flexible routing patterns can be supported. + * + * For example, asking for the URL for `auth` may return something + * like `https://backstage.example.com/api/auth` + */ + getBaseUrl(pluginId: string): Promise; +}; + +export const discoveryApiRef = createApiRef({ + id: 'core.discovery', + description: 'Provides service discovery of backend plugins', +}); diff --git a/packages/core-api/src/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts index c5d4a15117..678dce9e32 100644 --- a/packages/core-api/src/apis/definitions/index.ts +++ b/packages/core-api/src/apis/definitions/index.ts @@ -27,6 +27,7 @@ export * from './AppThemeApi'; export * from './ConfigApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; +export * from './DiscoveryApi'; export * from './IdentityApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; diff --git a/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts b/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts new file mode 100644 index 0000000000..9597443b98 --- /dev/null +++ b/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { UrlPatternDiscovery } from './UrlPatternDiscovery'; + +describe('UrlPatternDiscovery', () => { + it('should not require interpolation', async () => { + const discoveryApi = UrlPatternDiscovery.compile('http://example.com'); + await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe( + 'http://example.com', + ); + }); + + it('should use a plain pattern', async () => { + const discoveryApi = UrlPatternDiscovery.compile( + 'http://localhost:7000/{{ pluginId }}', + ); + await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe( + 'http://localhost:7000/my-plugin', + ); + }); + + it('should allow for multiple interpolation points', async () => { + const discoveryApi = UrlPatternDiscovery.compile( + 'https://{{pluginId }}.example.com/api/{{ pluginId}}', + ); + await expect(discoveryApi.getBaseUrl('my-plugin')).resolves.toBe( + 'https://my-plugin.example.com/api/my-plugin', + ); + }); + + it('should validate that the pattern is a valid URL', () => { + expect(() => { + UrlPatternDiscovery.compile('example.com'); + }).toThrow('Invalid discovery URL pattern, Invalid URL: example.com'); + + expect(() => { + UrlPatternDiscovery.compile('http://'); + }).toThrow('Invalid discovery URL pattern, Invalid URL: http://'); + + expect(() => { + UrlPatternDiscovery.compile('abc123'); + }).toThrow('Invalid discovery URL pattern, Invalid URL: abc123'); + + expect(() => { + UrlPatternDiscovery.compile('http://example.com:{{pluginId}}'); + }).toThrow( + 'Invalid discovery URL pattern, Invalid URL: http://example.com:pluginId', + ); + + expect(() => { + UrlPatternDiscovery.compile('/{{pluginId}}'); + }).toThrow('Invalid discovery URL pattern, Invalid URL: /pluginId'); + + expect(() => { + UrlPatternDiscovery.compile('http://localhost/{{pluginId}}?forbidden'); + }).toThrow('Invalid discovery URL pattern, URL must not have a query'); + + expect(() => { + UrlPatternDiscovery.compile('http://localhost/{{pluginId}}#forbidden'); + }).toThrow('Invalid discovery URL pattern, URL must not have a hash'); + + expect(() => { + UrlPatternDiscovery.compile('http://localhost/{{pluginId}}/'); + }).toThrow('Invalid discovery URL pattern, URL must not end with a slash'); + + expect(() => { + UrlPatternDiscovery.compile('http://localhost/'); + }).toThrow('Invalid discovery URL pattern, URL must not end with a slash'); + }); +}); diff --git a/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts b/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts new file mode 100644 index 0000000000..ca48784584 --- /dev/null +++ b/packages/core-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts @@ -0,0 +1,58 @@ +/* + * 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 { DiscoveryApi } from '../../definitions/DiscoveryApi'; + +/** + * UrlPatternDiscovery is a lightweight DiscoveryApi implementation. + * It uses a single template string to construct URLs for each plugin. + */ +export class UrlPatternDiscovery implements DiscoveryApi { + /** + * Creates a new UrlPatternDiscovery given a template. The the only + * interpolation done for the template is to replace instances of `{{pluginId}}` + * with the ID of the plugin being requested. + * + * Example pattern: `http://localhost:7000/api/{{ pluginId }}` + */ + static compile(pattern: string): UrlPatternDiscovery { + const parts = pattern.split(/\{\{\s*pluginId\s*\}\}/); + + try { + const urlStr = parts.join('pluginId'); + const url = new URL(urlStr); + if (url.hash) { + throw new Error('URL must not have a hash'); + } + if (url.search) { + throw new Error('URL must not have a query'); + } + if (urlStr.endsWith('/')) { + throw new Error('URL must not end with a slash'); + } + } catch (error) { + throw new Error(`Invalid discovery URL pattern, ${error.message}`); + } + + return new UrlPatternDiscovery(parts); + } + + private constructor(private readonly parts: string[]) {} + + async getBaseUrl(pluginId: string): Promise { + return this.parts.join(pluginId); + } +} diff --git a/packages/core-api/src/apis/implementations/DiscoveryApi/index.ts b/packages/core-api/src/apis/implementations/DiscoveryApi/index.ts new file mode 100644 index 0000000000..60a5b815e7 --- /dev/null +++ b/packages/core-api/src/apis/implementations/DiscoveryApi/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +// This folder contains implementations for all core APIs. +// +// Plugins should rely on these APIs for functionality as much as possible. + +export { UrlPatternDiscovery } from './UrlPatternDiscovery'; diff --git a/packages/core-api/src/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts index e6d23fee21..30aeb81d44 100644 --- a/packages/core-api/src/apis/implementations/index.ts +++ b/packages/core-api/src/apis/implementations/index.ts @@ -24,5 +24,6 @@ export * from './AlertApi'; export * from './AppThemeApi'; export * from './ConfigApi'; export * from './ErrorApi'; +export * from './DiscoveryApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; From ea1a36433d8b2b63e5a63e912f82f07d1dc336bf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 20 Aug 2020 17:56:33 +0200 Subject: [PATCH 02/68] app,core-api: install DiscoveryApi in default app and use it for all auth providers --- packages/app/src/apis.ts | 29 ++++++----- .../implementations/auth/auth0/Auth0Auth.ts | 17 +++--- .../implementations/auth/github/GithubAuth.ts | 17 +++--- .../implementations/auth/gitlab/GitlabAuth.ts | 16 +++--- .../implementations/auth/google/GoogleAuth.ts | 17 +++--- .../implementations/auth/oauth2/OAuth2.ts | 16 +++--- .../implementations/auth/okta/OktaAuth.ts | 16 +++--- .../DefaultAuthConnector.test.ts | 11 ++-- .../lib/AuthConnector/DefaultAuthConnector.ts | 52 +++++++++---------- packages/dev-utils/src/devApp/apiFactories.ts | 37 +++++++------ .../ProfileCatalog/ProfileCatalog.test.tsx | 6 ++- 11 files changed, 120 insertions(+), 114 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index df023b80cb..eb8cfb92af 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -24,6 +24,8 @@ import { ErrorAlerter, featureFlagsApiRef, FeatureFlags, + discoveryApiRef, + UrlPatternDiscovery, GoogleAuth, GithubAuth, OAuth2, @@ -74,7 +76,10 @@ import { TravisCIApi, travisCIApiRef, } from '@roadiehq/backstage-plugin-travis-ci'; -import { GithubPullRequestsClient, githubPullRequestsApiRef } from '@roadiehq/backstage-plugin-github-pull-requests'; +import { + GithubPullRequestsClient, + githubPullRequestsApiRef, +} from '@roadiehq/backstage-plugin-github-pull-requests'; export const apis = (config: ConfigApi) => { // eslint-disable-next-line no-console @@ -85,6 +90,10 @@ export const apis = (config: ConfigApi) => { const builder = ApiRegistry.builder(); + const discoveryApi = builder.add( + discoveryApiRef, + UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`), + ); const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); const errorApi = builder.add( errorApiRef, @@ -116,8 +125,7 @@ export const apis = (config: ConfigApi) => { builder.add( googleAuthApiRef, GoogleAuth.create({ - backendUrl, - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), ); @@ -125,8 +133,7 @@ export const apis = (config: ConfigApi) => { const githubAuthApi = builder.add( githubAuthApiRef, GithubAuth.create({ - backendUrl, - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), ); @@ -134,8 +141,7 @@ export const apis = (config: ConfigApi) => { builder.add( oktaAuthApiRef, OktaAuth.create({ - backendUrl, - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), ); @@ -143,8 +149,7 @@ export const apis = (config: ConfigApi) => { builder.add( gitlabAuthApiRef, GitlabAuth.create({ - backendUrl, - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), ); @@ -152,8 +157,7 @@ export const apis = (config: ConfigApi) => { builder.add( auth0AuthApiRef, Auth0Auth.create({ - backendUrl, - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), ); @@ -161,8 +165,7 @@ export const apis = (config: ConfigApi) => { builder.add( oauth2ApiRef, OAuth2.create({ - backendUrl, - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), ); diff --git a/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts index e69733741f..505c283b71 100644 --- a/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ b/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts @@ -27,16 +27,17 @@ import { AuthRequestOptions, BackstageIdentity, } from '../../../definitions/auth'; -import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { + OAuthRequestApi, + AuthProvider, + DiscoveryApi, +} from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { Observable } from '../../../../types'; type CreateOptions = { - // TODO(Following the words of Rugvip): These two should be grabbed from global config when available, they're not unique to Auth0Auth - backendUrl: string; - basePath: string; - + discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; environment?: string; @@ -67,15 +68,13 @@ class Auth0Auth BackstageIdentityApi, SessionStateApi { static create({ - backendUrl, - basePath, + discoveryApi, environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, }: CreateOptions) { const connector = new DefaultAuthConnector({ - backendUrl, - basePath, + discoveryApi, environment, provider, oauthRequestApi: oauthRequestApi, diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index 1ad3e08699..8b9f807cd8 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -25,7 +25,11 @@ import { BackstageIdentity, AuthRequestOptions, } from '../../../definitions/auth'; -import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { + OAuthRequestApi, + AuthProvider, + DiscoveryApi, +} from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { AuthSessionStore, @@ -34,10 +38,7 @@ import { import { Observable } from '../../../../types'; type CreateOptions = { - // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth - backendUrl: string; - basePath: string; - + discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; environment?: string; @@ -62,15 +63,13 @@ const DEFAULT_PROVIDER = { class GithubAuth implements OAuthApi, SessionStateApi { static create({ - backendUrl, - basePath, + discoveryApi, environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, }: CreateOptions) { const connector = new DefaultAuthConnector({ - backendUrl, - basePath, + discoveryApi, environment, provider, oauthRequestApi: oauthRequestApi, diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts index e612c4ca61..1734e930fa 100644 --- a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts @@ -25,15 +25,17 @@ import { BackstageIdentity, AuthRequestOptions, } from '../../../definitions/auth'; -import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { + OAuthRequestApi, + AuthProvider, + DiscoveryApi, +} from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { Observable } from '../../../../types'; type CreateOptions = { - backendUrl: string; - basePath: string; - + discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; environment?: string; @@ -58,15 +60,13 @@ const DEFAULT_PROVIDER = { class GitlabAuth implements OAuthApi, SessionStateApi { static create({ - backendUrl, - basePath, + discoveryApi, environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, }: CreateOptions) { const connector = new DefaultAuthConnector({ - backendUrl, - basePath, + discoveryApi, environment, provider, oauthRequestApi, diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts index c6a21dbb20..fdf9d46ba8 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -28,16 +28,17 @@ import { AuthRequestOptions, BackstageIdentity, } from '../../../definitions/auth'; -import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { + OAuthRequestApi, + AuthProvider, + DiscoveryApi, +} from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { Observable } from '../../../../types'; type CreateOptions = { - // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth - backendUrl: string; - basePath: string; - + discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; environment?: string; @@ -71,15 +72,13 @@ class GoogleAuth BackstageIdentityApi, SessionStateApi { static create({ - backendUrl, - basePath, + discoveryApi, environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, }: CreateOptions) { const connector = new DefaultAuthConnector({ - backendUrl, - basePath, + discoveryApi, environment, provider, oauthRequestApi: oauthRequestApi, diff --git a/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 4b6177bed9..cf582b3a13 100644 --- a/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -19,7 +19,11 @@ import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { Observable } from '../../../../types'; -import { AuthProvider, OAuthRequestApi } from '../../../definitions'; +import { + AuthProvider, + OAuthRequestApi, + DiscoveryApi, +} from '../../../definitions'; import { AuthRequestOptions, BackstageIdentity, @@ -33,9 +37,7 @@ import { import { OAuth2Session } from './types'; type CreateOptions = { - backendUrl: string; - basePath: string; - + discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; environment?: string; @@ -64,15 +66,13 @@ const SCOPE_PREFIX = ''; class OAuth2 implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi { static create({ - backendUrl, - basePath, + discoveryApi, environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, }: CreateOptions) { const connector = new DefaultAuthConnector({ - backendUrl, - basePath, + discoveryApi, environment, provider, oauthRequestApi: oauthRequestApi, diff --git a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts index f60b182118..1f804748f0 100644 --- a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts @@ -28,15 +28,17 @@ import { AuthRequestOptions, BackstageIdentity, } from '../../../definitions/auth'; -import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { + OAuthRequestApi, + AuthProvider, + DiscoveryApi, +} from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { Observable } from '../../../../types'; type CreateOptions = { - backendUrl: string; - basePath: string; - + discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; environment?: string; @@ -80,15 +82,13 @@ class OktaAuth BackstageIdentityApi, SessionStateApi { static create({ - backendUrl, - basePath, + discoveryApi, environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, }: CreateOptions) { const connector = new DefaultAuthConnector({ - backendUrl, - basePath, + discoveryApi, environment, provider, oauthRequestApi: oauthRequestApi, diff --git a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index b6c31405ae..5781130799 100644 --- a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -18,11 +18,12 @@ import ProviderIcon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from './DefaultAuthConnector'; import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi'; import * as loginPopup from '../loginPopup'; +import { UrlPatternDiscovery } from '../../apis'; const anyFetch = fetch as any; const defaultOptions = { - backendUrl: 'http://my-origin', + discoveryApi: UrlPatternDiscovery.compile('http://my-host/api/{{pluginId}}'), environment: 'production', provider: { id: 'my-provider', @@ -115,7 +116,7 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toBeCalledTimes(1); expect(popupSpy.mock.calls[0][0]).toMatchObject({ url: - 'http://my-origin/api/auth/my-provider/start?scope=a%20b&env=production', + 'http://my-host/api/auth/my-provider/start?scope=a%20b&env=production', }); await expect(sessionPromise).resolves.toEqual({ @@ -141,9 +142,9 @@ describe('DefaultAuthConnector', () => { instantPopup: true, }); - expect(popupSpy).toBeCalledTimes(1); - await expect(sessionPromise).resolves.toBe('my-session'); + + expect(popupSpy).toBeCalledTimes(1); }); it('should use join func to join scopes', async () => { @@ -164,7 +165,7 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toBeCalledTimes(1); expect(popupSpy.mock.calls[0][0]).toMatchObject({ url: - 'http://my-origin/api/auth/my-provider/start?scope=-ab-&env=production', + 'http://my-host/api/auth/my-provider/start?scope=-ab-&env=production', }); }); }); diff --git a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts index 41af7f4670..1c7e92daa8 100644 --- a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -15,21 +15,19 @@ */ import { AuthRequester } from '../../apis'; -import { OAuthRequestApi, AuthProvider } from '../../apis/definitions'; +import { + OAuthRequestApi, + AuthProvider, + DiscoveryApi, +} from '../../apis/definitions'; import { showLoginPopup } from '../loginPopup'; import { AuthConnector, CreateSessionOptions } from './types'; -const DEFAULT_BASE_PATH = '/api/auth/'; - type Options = { /** - * The base URL of the auth backend. + * DiscoveryApi instance used to locate the auth backend endpoint. */ - backendUrl?: string; - /** - * Base path of the auth requests, defaults to /api/auth/ - */ - basePath?: string; + discoveryApi: DiscoveryApi; /** * Environment hint passed on to auth backend, for example 'production' or 'development' */ @@ -64,8 +62,7 @@ function defaultJoinScopes(scopes: Set) { */ export class DefaultAuthConnector implements AuthConnector { - private readonly backendUrl: string; - private readonly basePath: string; + private readonly discoveryApi: DiscoveryApi; private readonly environment: string; private readonly provider: AuthProvider & { id: string }; private readonly joinScopesFunc: (scopes: Set) => string; @@ -74,8 +71,7 @@ export class DefaultAuthConnector constructor(options: Options) { const { - backendUrl = window.location.origin, - basePath = DEFAULT_BASE_PATH, + discoveryApi, environment, provider, joinScopes = defaultJoinScopes, @@ -88,8 +84,7 @@ export class DefaultAuthConnector onAuthRequest: scopes => this.showPopup(scopes), }); - this.backendUrl = backendUrl; - this.basePath = basePath; + this.discoveryApi = discoveryApi; this.environment = environment; this.provider = provider; this.joinScopesFunc = joinScopes; @@ -104,12 +99,15 @@ export class DefaultAuthConnector } async refreshSession(): Promise { - const res = await fetch(this.buildUrl('/refresh', { optional: true }), { - headers: { - 'x-requested-with': 'XMLHttpRequest', + const res = await fetch( + await this.buildUrl('/refresh', { optional: true }), + { + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + credentials: 'include', }, - credentials: 'include', - }).catch(error => { + ).catch(error => { throw new Error(`Auth refresh request failed, ${error}`); }); @@ -134,7 +132,7 @@ export class DefaultAuthConnector } async removeSession(): Promise { - const res = await fetch(this.buildUrl('/logout'), { + const res = await fetch(await this.buildUrl('/logout'), { method: 'POST', headers: { 'x-requested-with': 'XMLHttpRequest', @@ -153,13 +151,12 @@ export class DefaultAuthConnector private async showPopup(scopes: Set): Promise { const scope = this.joinScopesFunc(scopes); - const popupUrl = this.buildUrl('/start', { scope }); - const { origin } = new URL(this.backendUrl); + const popupUrl = await this.buildUrl('/start', { scope }); const payload = await showLoginPopup({ url: popupUrl, name: `${this.provider.title} Login`, - origin, + origin: new URL(popupUrl).origin, width: 450, height: 730, }); @@ -167,16 +164,17 @@ export class DefaultAuthConnector return await this.sessionTransform(payload); } - private buildUrl( + private async buildUrl( path: string, query?: { [key: string]: string | boolean | undefined }, - ): string { + ): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('auth'); const queryString = this.buildQueryString({ ...query, env: this.environment, }); - return `${this.backendUrl}${this.basePath}${this.provider.id}${path}${queryString}`; + return `${baseUrl}/${this.provider.id}${path}${queryString}`; } private buildQueryString(query?: { diff --git a/packages/dev-utils/src/devApp/apiFactories.ts b/packages/dev-utils/src/devApp/apiFactories.ts index 5acf64e0d0..4cf208f5d2 100644 --- a/packages/dev-utils/src/devApp/apiFactories.ts +++ b/packages/dev-utils/src/devApp/apiFactories.ts @@ -24,6 +24,8 @@ import { AlertApiForwarder, oauthRequestApiRef, OAuthRequestManager, + UrlPatternDiscovery, + discoveryApiRef, GoogleAuth, googleAuthApiRef, GithubAuth, @@ -58,46 +60,49 @@ export const oauthRequestApiFactory = createApiFactory({ factory: () => new OAuthRequestManager(), }); +export const discoveryApiFactory = createApiFactory({ + implements: discoveryApiRef, + deps: {}, + factory: () => + UrlPatternDiscovery.compile(`http://localhost:7000/{{ pluginId }}`), +}); + export const googleAuthApiFactory = createApiFactory({ implements: googleAuthApiRef, - deps: { oauthRequestApi: oauthRequestApiRef }, - factory: ({ oauthRequestApi }) => + deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef }, + factory: ({ discoveryApi, oauthRequestApi }) => GoogleAuth.create({ - backendUrl: 'http://localhost:7000', - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), }); export const githubAuthApiFactory = createApiFactory({ implements: githubAuthApiRef, - deps: { oauthRequestApi: oauthRequestApiRef }, - factory: ({ oauthRequestApi }) => + deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef }, + factory: ({ discoveryApi, oauthRequestApi }) => GithubAuth.create({ - backendUrl: 'http://localhost:7000', - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), }); export const gitlabAuthApiFactory = createApiFactory({ implements: gitlabAuthApiRef, - deps: { oauthRequestApi: oauthRequestApiRef }, - factory: ({ oauthRequestApi }) => + deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef }, + factory: ({ discoveryApi, oauthRequestApi }) => GitlabAuth.create({ - backendUrl: 'http://localhost:7000', - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), }); export const auth0AuthApiFactory = createApiFactory({ implements: auth0AuthApiRef, - deps: { oauthRequestApi: oauthRequestApiRef }, - factory: ({ oauthRequestApi }) => + deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef }, + factory: ({ discoveryApi, oauthRequestApi }) => Auth0Auth.create({ - backendUrl: 'http://localhost:7000', - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), }); diff --git a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx index a502f6fb4e..06074ff84d 100644 --- a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx +++ b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx @@ -26,6 +26,7 @@ import { githubAuthApiRef, GithubAuth, OAuthRequestManager, + UrlPatternDiscovery, } from '@backstage/core'; import { gitOpsApiRef, GitOpsRestApi } from '../../api'; @@ -37,8 +38,9 @@ describe('ProfileCatalog', () => { [ githubAuthApiRef, GithubAuth.create({ - backendUrl: 'http://localhost:7000', - basePath: '/auth/', + discoveryApi: UrlPatternDiscovery.compile( + 'http://example.com/{{pluginId}}', + ), oauthRequestApi, }), ], From 382491dc5af1ca2567a93a1be4b218b4b08a93e6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 21 Aug 2020 14:25:09 +0200 Subject: [PATCH 03/68] plugins/catalog: switch CatalogClient to use DiscoveryApi --- packages/app/src/apis.ts | 8 +---- plugins/catalog/src/api/CatalogClient.test.ts | 33 ++++++++----------- plugins/catalog/src/api/CatalogClient.ts | 23 +++++-------- 3 files changed, 22 insertions(+), 42 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index eb8cfb92af..c787ffc589 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -178,13 +178,7 @@ export const apis = (config: ConfigApi) => { }), ); - builder.add( - catalogApiRef, - new CatalogClient({ - apiOrigin: backendUrl, - basePath: '/catalog', - }), - ); + builder.add(catalogApiRef, new CatalogClient({ discoveryApi })); builder.add( scaffolderApiRef, diff --git a/plugins/catalog/src/api/CatalogClient.test.ts b/plugins/catalog/src/api/CatalogClient.test.ts index 7803f2e173..18f062db77 100644 --- a/plugins/catalog/src/api/CatalogClient.test.ts +++ b/plugins/catalog/src/api/CatalogClient.test.ts @@ -18,25 +18,21 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { CatalogClient } from './CatalogClient'; import { Entity } from '@backstage/catalog-model'; +import { UrlPatternDiscovery } from '@backstage/core'; const server = setupServer(); +const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; +const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); describe('CatalogClient', () => { beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); - const mockApiOrigin = 'http://backstage:9191'; - const mockBasePath = '/i-am-a-mock-base'; - let client = new CatalogClient({ - apiOrigin: mockApiOrigin, - basePath: mockBasePath, - }); + + let client = new CatalogClient({ discoveryApi }); beforeEach(() => { - client = new CatalogClient({ - apiOrigin: mockApiOrigin, - basePath: mockBasePath, - }); + client = new CatalogClient({ discoveryApi }); }); describe('getEntiies', () => { @@ -61,7 +57,7 @@ describe('CatalogClient', () => { beforeEach(() => { server.use( - rest.get(`${mockApiOrigin}${mockBasePath}/entities`, (_, res, ctx) => { + rest.get(`${mockBaseUrl}/entities`, (_, res, ctx) => { return res(ctx.json(defaultResponse)); }), ); @@ -75,15 +71,12 @@ describe('CatalogClient', () => { it('builds entity search filters properly', async () => { expect.assertions(2); server.use( - rest.get( - `${mockApiOrigin}${mockBasePath}/entities`, - (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe( - 'a=1&b=2&b=3&%C3%B6=%3D', - ); - return res(ctx.json([])); - }, - ), + rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe( + 'a=1&b=2&b=3&%C3%B6=%3D', + ); + return res(ctx.json([])); + }), ); const entities = await client.getEntities({ diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 3804315ace..d5ff033caa 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -20,24 +20,17 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { CatalogApi, EntityCompoundName } from './types'; +import { DiscoveryApi } from '@backstage/core'; export class CatalogClient implements CatalogApi { - private apiOrigin: string; - private basePath: string; + private readonly discoveryApi: DiscoveryApi; - constructor({ - apiOrigin, - basePath, - }: { - apiOrigin: string; - basePath: string; - }) { - this.apiOrigin = apiOrigin; - this.basePath = basePath; + constructor(options: { discoveryApi: DiscoveryApi }) { + this.discoveryApi = options.discoveryApi; } private async getRequired(path: string): Promise { - const url = `${this.apiOrigin}${this.basePath}${path}`; + const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; const response = await fetch(url); if (!response.ok) { @@ -50,7 +43,7 @@ export class CatalogClient implements CatalogApi { } private async getOptional(path: string): Promise { - const url = `${this.apiOrigin}${this.basePath}${path}`; + const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`; const response = await fetch(url); if (!response.ok) { @@ -100,7 +93,7 @@ export class CatalogClient implements CatalogApi { async addLocation(type: string, target: string) { const response = await fetch( - `${this.apiOrigin}${this.basePath}/locations`, + `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, { headers: { 'Content-Type': 'application/json', @@ -135,7 +128,7 @@ export class CatalogClient implements CatalogApi { async removeEntityByUid(uid: string): Promise { const response = await fetch( - `${this.apiOrigin}${this.basePath}/entities/by-uid/${uid}`, + `${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`, { method: 'DELETE', }, From be837b7b361965aee91becb5e56ed2bda5127291 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 21 Aug 2020 14:28:23 +0200 Subject: [PATCH 04/68] plugins/scaffolder: switch ScaffolderApi to use DiscoveryApi --- packages/app/src/apis.ts | 8 +------- plugins/scaffolder/src/api.ts | 24 ++++++++---------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index c787ffc589..72dad858c7 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -180,13 +180,7 @@ export const apis = (config: ConfigApi) => { builder.add(catalogApiRef, new CatalogClient({ discoveryApi })); - builder.add( - scaffolderApiRef, - new ScaffolderApi({ - apiOrigin: backendUrl, - basePath: '/scaffolder/v1', - }), - ); + builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi })); builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')); diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 4a0d508a2b..3c42ed2ca4 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '@backstage/core'; +import { createApiRef, DiscoveryApi } from '@backstage/core'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; export const scaffolderApiRef = createApiRef({ @@ -23,18 +23,10 @@ export const scaffolderApiRef = createApiRef({ }); export class ScaffolderApi { - private apiOrigin: string; - private basePath: string; + private readonly discoveryApi: DiscoveryApi; - constructor({ - apiOrigin, - basePath, - }: { - apiOrigin: string; - basePath: string; - }) { - this.apiOrigin = apiOrigin; - this.basePath = basePath; + constructor(options: { discoveryApi: DiscoveryApi }) { + this.discoveryApi = options.discoveryApi; } /** @@ -46,7 +38,7 @@ export class ScaffolderApi { template: TemplateEntityV1alpha1, values: Record, ) { - const url = `${this.apiOrigin}${this.basePath}/jobs`; + const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v1/jobs`; const response = await fetch(url, { method: 'POST', headers: { @@ -65,9 +57,9 @@ export class ScaffolderApi { } async getJob(jobId: string) { - const url = `${this.apiOrigin}${this.basePath}/job/${encodeURIComponent( - jobId, - )}`; + const url = `${await this.discoveryApi.getBaseUrl( + 'scaffolder', + )}/v1/job/${encodeURIComponent(jobId)}`; return fetch(url).then(x => x.json()); } } From 6f0c438519b7b3deae60440090d163a317d90740 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 21 Aug 2020 15:13:52 +0200 Subject: [PATCH 05/68] plugins/rollbar: switch RollbarClient to use DiscoveryApi --- packages/app/src/apis.ts | 8 +------- plugins/rollbar/src/api/RollbarClient.ts | 17 +++++------------ 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 72dad858c7..0c409994cb 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -201,13 +201,7 @@ export const apis = (config: ConfigApi) => { ]), ); - builder.add( - rollbarApiRef, - new RollbarClient({ - apiOrigin: backendUrl, - basePath: '/rollbar', - }), - ); + builder.add(rollbarApiRef, new RollbarClient({ discoveryApi })); builder.add( techdocsStorageApiRef, diff --git a/plugins/rollbar/src/api/RollbarClient.ts b/plugins/rollbar/src/api/RollbarClient.ts index 5cabbcbc24..1862ad8270 100644 --- a/plugins/rollbar/src/api/RollbarClient.ts +++ b/plugins/rollbar/src/api/RollbarClient.ts @@ -20,20 +20,13 @@ import { RollbarProject, RollbarTopActiveItem, } from './types'; +import { DiscoveryApi } from '@backstage/core'; export class RollbarClient implements RollbarApi { - private apiOrigin: string; - private basePath: string; + private readonly discoveryApi: DiscoveryApi; - constructor({ - apiOrigin, - basePath, - }: { - apiOrigin: string; - basePath: string; - }) { - this.apiOrigin = apiOrigin; - this.basePath = basePath; + constructor(options: { discoveryApi: DiscoveryApi }) { + this.discoveryApi = options.discoveryApi; } async getAllProjects(): Promise { @@ -59,7 +52,7 @@ export class RollbarClient implements RollbarApi { } private async get(path: string): Promise { - const url = `${this.apiOrigin}${this.basePath}${path}`; + const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`; const response = await fetch(url); if (!response.ok) { From a21cfcd062c2f2944197d4a36fbefb90b5971c22 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Aug 2020 11:11:10 +0200 Subject: [PATCH 06/68] create-app: update template to use DiscoveryApi --- .../default-app/packages/app/src/apis.ts | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/create-app/templates/default-app/packages/app/src/apis.ts b/packages/create-app/templates/default-app/packages/app/src/apis.ts index 35f3e076d6..4cc2ebe03a 100644 --- a/packages/create-app/templates/default-app/packages/app/src/apis.ts +++ b/packages/create-app/templates/default-app/packages/app/src/apis.ts @@ -6,6 +6,8 @@ import { ConfigApi, ErrorApiForwarder, ErrorAlerter, + discoveryApiRef, + UrlPatternDiscovery, oauthRequestApiRef, OAuthRequestManager, storageApiRef, @@ -24,6 +26,10 @@ export const apis = (config: ConfigApi) => { const builder = ApiRegistry.builder(); + const discoveryApi = builder.add( + discoveryApiRef, + UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`), + ); const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); const errorApi = builder.add( errorApiRef, @@ -33,21 +39,9 @@ export const apis = (config: ConfigApi) => { builder.add(storageApiRef, WebStorage.create({ errorApi })); builder.add(oauthRequestApiRef, new OAuthRequestManager()); - builder.add( - catalogApiRef, - new CatalogClient({ - apiOrigin: backendUrl, - basePath: '/catalog', - }), - ); + builder.add(catalogApiRef, new CatalogClient({ discoveryApi })); - builder.add( - scaffolderApiRef, - new ScaffolderApi({ - apiOrigin: backendUrl, - basePath: '/scaffolder/v1', - }), - ); + builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi })); return builder.build(); }; From 257a3b52ed64d82230b270a2c04a9543fc1f42ec Mon Sep 17 00:00:00 2001 From: Omer Farooq Date: Sun, 23 Aug 2020 00:17:57 +1200 Subject: [PATCH 07/68] Add Azure ingestion processor --- .../src/ingestion/LocationReaders.ts | 2 + .../AzureApiReaderProcessor.test.ts | 84 +++++++++++ .../processors/AzureApiReaderProcessor.ts | 134 ++++++++++++++++++ .../RegisterComponentForm.tsx | 2 +- .../RegisterComponentPage.tsx | 13 +- 5 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 05f93815b8..29892bb2dc 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -31,6 +31,7 @@ import { GithubApiReaderProcessor } from './processors/GithubApiReaderProcessor' import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor'; import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor'; import { BitbucketApiReaderProcessor } from './processors/BitbucketApiReaderProcessor'; +import { AzureApiReaderProcessor } from './processors/AzureApiReaderProcessor'; import { UrlReaderProcessor } from './processors/UrlReaderProcessor'; import { LocationRefProcessor } from './processors/LocationEntityProcessor'; import { StaticLocationProcessor } from './processors/StaticLocationProcessor'; @@ -79,6 +80,7 @@ export class LocationReaders implements LocationReader { new GitlabApiReaderProcessor(), new GitlabReaderProcessor(), new BitbucketApiReaderProcessor(), + new AzureApiReaderProcessor(), new UrlReaderProcessor(), new YamlProcessor(), new EntityPolicyProcessor(entityPolicy), diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts new file mode 100644 index 0000000000..35101b2607 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { AzureApiReaderProcessor } from './AzureApiReaderProcessor'; + +describe('BitbucketApiReaderProcessor', () => { + it('should build raw api', () => { + const processor = new AzureApiReaderProcessor(); + const tests = [ + { + target: + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster', + url: new URL( + 'https://dev.azure.com/org-name/project-name/_apis/sourceProviders/TfsGit/filecontents?repository=repo-name&commitOrBranch=master&path=my-template.yaml&api-version=6.0-preview.1', + ), + err: undefined, + }, + { + target: 'https://api.com/a/b/blob/master/path/to/c.yaml', + url: null, + err: + 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path', + }, + { + target: 'com/a/b/blob/master/path/to/c.yaml', + url: null, + err: + 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', + }, + ]; + + for (const test of tests) { + if (test.err) { + expect(() => processor.buildRawUrl(test.target)).toThrowError(test.err); + } else if (test.url) { + expect(processor.buildRawUrl(test.target).toString()).toEqual( + test.url.toString(), + ); + } else { + throw new Error( + 'This should not have happened. Either err or url should have matched.', + ); + } + } + }); + + it('should return request options', () => { + const tests = [ + { + token: '0123456789', + expect: { + headers: { + Authorization: 'Basic OjAxMjM0NTY3ODk=', + }, + }, + }, + { + token: '', + expect: { + headers: {}, + }, + }, + ]; + + for (const test of tests) { + process.env.AZURE_PRIVATE_TOKEN = test.token; + const processor = new AzureApiReaderProcessor(); + expect(processor.getRequestOptions()).toEqual(test.expect); + } + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts new file mode 100644 index 0000000000..41b4336b4f --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts @@ -0,0 +1,134 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LocationSpec } from '@backstage/catalog-model'; +import fetch, { RequestInit, HeadersInit } from 'node-fetch'; +import * as result from './results'; +import { LocationProcessor, LocationProcessorEmit } from './types'; + +export class AzureApiReaderProcessor implements LocationProcessor { + private privateToken: string = process.env.AZURE_PRIVATE_TOKEN || ''; + + getRequestOptions(): RequestInit { + const headers: HeadersInit = {}; + + if (this.privateToken !== '') { + headers.Authorization = `Basic ${Buffer.from( + `:${this.privateToken}`, + 'utf8', + ).toString('base64')}`; + } + + const requestOptions: RequestInit = { + headers, + }; + + return requestOptions; + } + + async readLocation( + location: LocationSpec, + optional: boolean, + emit: LocationProcessorEmit, + ): Promise { + if (location.type !== 'azure/api') { + return false; + } + + try { + const url = this.buildRawUrl(location.target); + + const response = await fetch(url.toString(), this.getRequestOptions()); + + if (response.ok) { + const data = await response.buffer(); + emit(result.data(location, data)); + } else { + const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + if (!optional) { + emit(result.notFoundError(location, message)); + } + } else { + emit(result.generalError(location, message)); + } + } + } catch (e) { + const message = `Unable to read ${location.type} ${location.target}, ${e}`; + emit(result.generalError(location, message)); + } + return true; + } + + // Converts + // from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents + // to: https://dev.azure.com/{organization}/{project}/_apis/sourceProviders/{providerName}/filecontents?repository={repository}&commitOrBranch={commitOrBranch}&path={path}&api-version=6.0-preview.1 + + buildRawUrl(target: string): URL { + try { + const url = new URL(target); + + const [ + empty, + userOrOrg, + project, + srcKeyword, + repoName, + ] = url.pathname.split('/'); + + const path = url.searchParams.get('path') || ''; + const ref = url.searchParams.get('version')?.substr(2); + + if ( + url.hostname !== 'dev.azure.com' || + empty !== '' || + userOrOrg === '' || + project === '' || + srcKeyword !== '_git' || + repoName === '' || + path === '' || + ref === '' || + !path.match(/\.yaml$/) + ) { + throw new Error('Wrong Azure Devops URL or Invalid file path'); + } + + // transform to api + url.pathname = [ + empty, + userOrOrg, + project, + '_apis', + 'sourceProviders', + 'TfsGit', + 'filecontents', + ].join('/'); + + url.search = [ + `repository=${repoName}`, + `commitOrBranch=${ref}`, + `path=${path}`, + 'api-version=6.0-preview.1', + ].join('&'); + + url.protocol = 'https'; + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } + } +} diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx index c1ea9c454d..a61469e969 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx @@ -71,7 +71,7 @@ const RegisterComponentForm: FC = ({ onSubmit, submitting }) => { name="componentLocation" required margin="normal" - helperText="Enter the full path to the component.yaml file in GitHub to start tracking your component. It must be in a public repo." + helperText="Enter the full path to the component.yaml file in Github, Gitlab, Bitbucket or Azure to start tracking your component. For private repo provide authentication information via config." inputRef={register({ required: true, validate: ComponentIdValidators, diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 85d63fe2cb..be497e9aa7 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -79,7 +79,18 @@ const RegisterComponentPage: FC<{}> = () => { setFormState(FormStates.Submitting); const { componentLocation: target } = formData; try { - const data = await catalogApi.addLocation('github', target); + var typeMapping = [ + { url: /https:\/\/gitlab\.com\/.*/, type: 'gitlab' }, + { url: /https:\/\/bitbucket\.org\/.*/, type: 'bitbucket/api' }, + { url: /https:\/\/dev\.azure\.com\/.*/, type: 'azure/api' }, + { url: /.*/, type: 'github' }, + ]; + + var type = typeMapping.filter(function (item) { + return new RegExp(item.url).test(target); + })[0].type; + + const data = await catalogApi.addLocation(type, target); if (!isMounted()) return; From c33fc6c356227a1f5b034350630a879c413e85be Mon Sep 17 00:00:00 2001 From: Omer Farooq Date: Sun, 23 Aug 2020 00:41:58 +1200 Subject: [PATCH 08/68] Fix linting errors --- .../RegisterComponentPage/RegisterComponentPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index be497e9aa7..1f0eb4fead 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -79,14 +79,14 @@ const RegisterComponentPage: FC<{}> = () => { setFormState(FormStates.Submitting); const { componentLocation: target } = formData; try { - var typeMapping = [ + const typeMapping = [ { url: /https:\/\/gitlab\.com\/.*/, type: 'gitlab' }, { url: /https:\/\/bitbucket\.org\/.*/, type: 'bitbucket/api' }, { url: /https:\/\/dev\.azure\.com\/.*/, type: 'azure/api' }, { url: /.*/, type: 'github' }, ]; - var type = typeMapping.filter(function (item) { + const type = typeMapping.filter(item => { return new RegExp(item.url).test(target); })[0].type; From 7e454952f7e623fde4ea0437ed98e08c9a6ac0b8 Mon Sep 17 00:00:00 2001 From: Omer Farooq Date: Sun, 23 Aug 2020 00:59:03 +1200 Subject: [PATCH 09/68] fix broken UI test --- .../RegisterComponentForm/RegisterComponentForm.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx index 01402ca19b..fd7a1cc0f8 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx @@ -30,14 +30,14 @@ const setup = (props?: Partial) => { ), }; }; -describe('RegisterComponentForm', () => { +fdescribe('RegisterComponentForm', () => { afterEach(() => cleanup()); it('should initially render a disabled button', async () => { const { rendered } = setup(); expect( await rendered.findByText( - 'Enter the full path to the component.yaml file in GitHub to start tracking your component. It must be in a public repo.', + 'Enter the full path to the component.yaml file in Github, Gitlab, Bitbucket or Azure to start tracking your component. For private repo provide authentication information via config.', ), ).toBeInTheDocument(); From 54b6a9cc06d17e3365096c3314c4e78e4bb87cca Mon Sep 17 00:00:00 2001 From: Omer Farooq Date: Sun, 23 Aug 2020 01:02:32 +1200 Subject: [PATCH 10/68] undo test focus --- .../RegisterComponentForm/RegisterComponentForm.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx index fd7a1cc0f8..d4d2cf4789 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx @@ -30,7 +30,7 @@ const setup = (props?: Partial) => { ), }; }; -fdescribe('RegisterComponentForm', () => { +describe('RegisterComponentForm', () => { afterEach(() => cleanup()); it('should initially render a disabled button', async () => { From f57f6a7402965d7c95eb0b73af2608f779770e99 Mon Sep 17 00:00:00 2001 From: Omer Farooq Date: Sun, 23 Aug 2020 14:26:39 +1200 Subject: [PATCH 11/68] modify ends with .yaml check to contains .yaml --- .../processors/AzureApiReaderProcessor.test.ts | 2 +- .../ingestion/processors/YamlProcessor.test.ts | 16 ++++++++++++++++ .../src/ingestion/processors/YamlProcessor.ts | 2 +- .../RegisterComponentPage.tsx | 4 +--- .../register-component/src/util/validate.test.ts | 3 ++- plugins/register-component/src/util/validate.ts | 4 ++-- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts index 35101b2607..0ef111fcb1 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts @@ -16,7 +16,7 @@ import { AzureApiReaderProcessor } from './AzureApiReaderProcessor'; -describe('BitbucketApiReaderProcessor', () => { +describe('AzureApiReaderProcessor', () => { it('should build raw api', () => { const processor = new AzureApiReaderProcessor(); const tests = [ diff --git a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts index 587793a6de..9f1ede3f9e 100644 --- a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts @@ -51,6 +51,22 @@ describe('YamlProcessor', () => { expect(never).not.toBeCalled(); }); + it('should process url that contains yaml', async () => { + const containsYamlLocationSpec = { + type: 'url', + target: 'http://example.com/component?path=test.yaml&c=1&d=2', + }; + + const buffer = Buffer.from([]); + const emit = jest.fn(); + + expect( + await processor.parseData(buffer, containsYamlLocationSpec, emit), + ).toBe(true); + + expect(emit).toBeCalled(); + }); + it('should process entity with yaml', async () => { const entity = { apiVersion: 'backstage.io/v1alpha1', diff --git a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts index 6a2b5cf419..79ae55fae1 100644 --- a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts @@ -26,7 +26,7 @@ export class YamlProcessor implements LocationProcessor { location: LocationSpec, emit: LocationProcessorEmit, ): Promise { - if (!location.target.match(/\.ya?ml$/)) { + if (!location.target.match(/\.ya?ml/)) { return false; } diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 1f0eb4fead..e61f904347 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -86,9 +86,7 @@ const RegisterComponentPage: FC<{}> = () => { { url: /.*/, type: 'github' }, ]; - const type = typeMapping.filter(item => { - return new RegExp(item.url).test(target); - })[0].type; + const type = typeMapping.filter(item => item.url.test(target))[0].type; const data = await catalogApi.addLocation(type, target); diff --git a/plugins/register-component/src/util/validate.test.ts b/plugins/register-component/src/util/validate.test.ts index d062655bd2..e4fc90699b 100644 --- a/plugins/register-component/src/util/validate.test.ts +++ b/plugins/register-component/src/util/validate.test.ts @@ -31,11 +31,12 @@ describe('ComponentIdValidators', () => { }); }); describe('yamlValidator', () => { - const errorMessage = "Must end with '.yaml'."; + const errorMessage = "Must contain '.yaml'."; test.each([ [true, '.yaml'], [true, 'http://example.com/blob/master/service.yaml'], [true, 'https://example.yaml'], + [true, 'https://example.com?path=abc.yaml&c=1'], [errorMessage, '.yml'], [errorMessage, 'http://example.com/blob/master/service'], [errorMessage, undefined], diff --git a/plugins/register-component/src/util/validate.ts b/plugins/register-component/src/util/validate.ts index 78d20995f5..8552872015 100644 --- a/plugins/register-component/src/util/validate.ts +++ b/plugins/register-component/src/util/validate.ts @@ -19,6 +19,6 @@ export const ComponentIdValidators = { (typeof value === 'string' && value.match(/^https:\/\//) !== null) || 'Must start with https://.', yamlValidator: (value: any) => - (typeof value === 'string' && value.match(/.yaml$/) !== null) || - "Must end with '.yaml'.", + (typeof value === 'string' && value.match(/.yaml/) !== null) || + "Must contain '.yaml'.", }; From ba8bef38a637a3544b0aaae56a839fefcb393e56 Mon Sep 17 00:00:00 2001 From: Omer Farooq Date: Tue, 25 Aug 2020 18:30:12 +1200 Subject: [PATCH 12/68] update casing --- .../RegisterComponentForm/RegisterComponentForm.test.tsx | 2 +- .../components/RegisterComponentForm/RegisterComponentForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx index d4d2cf4789..e1226bbf32 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx @@ -37,7 +37,7 @@ describe('RegisterComponentForm', () => { const { rendered } = setup(); expect( await rendered.findByText( - 'Enter the full path to the component.yaml file in Github, Gitlab, Bitbucket or Azure to start tracking your component. For private repo provide authentication information via config.', + 'Enter the full path to the component.yaml file in GitHub, GitLab, Bitbucket or Azure to start tracking your component. For private repo provide authentication information via config.', ), ).toBeInTheDocument(); diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx index a61469e969..de3c54610d 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx @@ -71,7 +71,7 @@ const RegisterComponentForm: FC = ({ onSubmit, submitting }) => { name="componentLocation" required margin="normal" - helperText="Enter the full path to the component.yaml file in Github, Gitlab, Bitbucket or Azure to start tracking your component. For private repo provide authentication information via config." + helperText="Enter the full path to the component.yaml file in GitHub, GitLab, Bitbucket or Azure to start tracking your component. For private repo provide authentication information via config." inputRef={register({ required: true, validate: ComponentIdValidators, From 118e4861be8d791119614ea8acaa7fbc51aed366 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Tue, 25 Aug 2020 11:27:56 +0100 Subject: [PATCH 13/68] Use cookie cutter installed on host --- plugins/scaffolder-backend/package.json | 1 + .../stages/templater/cookiecutter.ts | 43 ++++++++++++------- .../scaffolder/stages/templater/helpers.ts | 43 +++++++++++++++++++ yarn.lock | 5 +++ 4 files changed, 76 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 7112d0f49d..275da3700b 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -27,6 +27,7 @@ "@octokit/rest": "^18.0.0", "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", + "command-exists-promise": "^2.0.2", "compression": "^1.7.4", "cors": "^2.8.5", "dockerode": "^3.2.0", diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts index fff349cf82..21b6fc3dde 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts @@ -15,11 +15,13 @@ */ import fs from 'fs-extra'; import { JsonValue } from '@backstage/config'; -import { runDockerContainer } from './helpers'; +import { runDockerContainer, runCommand } from './helpers'; import { TemplaterBase, TemplaterRunOptions } from '.'; import path from 'path'; import { TemplaterRunResult } from './types'; +const commandExists = require('command-exists-promise'); + export class CookieCutter implements TemplaterBase { private async fetchTemplateCookieCutter( directory: string, @@ -51,21 +53,30 @@ export class CookieCutter implements TemplaterBase { const templateDir = options.directory; const resultDir = await fs.promises.mkdtemp(`${options.directory}-result`); - await runDockerContainer({ - imageName: 'spotify/backstage-cookiecutter', - args: [ - 'cookiecutter', - '--no-input', - '-o', - '/result', - '/template', - '--verbose', - ], - templateDir, - resultDir, - logStream: options.logStream, - dockerClient: options.dockerClient, - }); + const cookieCutterInstalled = await commandExists('cookiecutter'); + if (cookieCutterInstalled) { + await runCommand({ + command: 'cookiecutter', + args: ['--no-input', '-o', resultDir, templateDir, '--verbose'], + logStream: options.logStream, + }); + } else { + await runDockerContainer({ + imageName: 'spotify/backstage-cookiecutter', + args: [ + 'cookiecutter', + '--no-input', + '-o', + '/result', + '/template', + '--verbose', + ], + templateDir, + resultDir, + logStream: options.logStream, + dockerClient: options.dockerClient, + }); + } return { resultDir: path.resolve(resultDir, options.values.component_id as string), diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts index a3dea7ea5a..e71b63bfb0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts @@ -18,6 +18,7 @@ import Docker from 'dockerode'; import fs from 'fs'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { InputError } from '@backstage/backend-common'; +import { spawn } from 'child_process'; export type RunDockerContainerOptions = { imageName: string; @@ -29,6 +30,12 @@ export type RunDockerContainerOptions = { createOptions?: Docker.ContainerCreateOptions; }; +export type RunCommandOptions = { + command: string; + args: string[]; + logStream?: Writable; +}; + /** * Gets the templater key to use for templating from the entity * @param entity Template entity @@ -43,6 +50,42 @@ export const getTemplaterKey = (entity: TemplateEntityV1alpha1): string => { return templater; }; +/** + * + * @param options the options object + * @param options.command the command to run + * @param options.args the arguments to pass the command + * @param options.logStream the log streamer to capture log messages + */ +export const runCommand = async ({ + command, + args, + logStream = new PassThrough(), +}: RunCommandOptions) => { + await new Promise((resolve, reject) => { + const process = spawn(command, args); + + process.stdout.on('data', stream => { + logStream.write(stream); + }); + + process.stderr.on('data', stream => { + logStream.write(stream); + }); + + process.on('error', error => { + return reject(error); + }); + + process.on('close', code => { + if (code !== 0) { + return reject(`Command ${command} failed, exit code: ${code}`); + } + return resolve(); + }); + }); +}; + /** * * @param options the options object diff --git a/yarn.lock b/yarn.lock index 857b8b2cc0..6f725fe353 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8104,6 +8104,11 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== +command-exists-promise@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/command-exists-promise/-/command-exists-promise-2.0.2.tgz#7beecc4b218299f3c61fa69a4047aa0b36a64a99" + integrity sha512-T6PB6vdFrwnHXg/I0kivM3DqaCGZLjjYSOe0a5WgFKcz1sOnmOeIjnhQPXVXX3QjVbLyTJ85lJkX6lUpukTzaA== + commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1, commander@~2.20.3: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" From 3ca9863f039e9f86c2c567fcf47e50ba49061bd1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 25 Aug 2020 15:24:02 +0200 Subject: [PATCH 14/68] cli: introduce jest transformModules options for providing a list of modules to transform --- package.json | 5 ++ packages/cli/config/jest.js | 87 +++++++++++-------- .../templates/default-app/package.json.hbs | 5 ++ 3 files changed, 60 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index e4aa18f540..e6ee961701 100644 --- a/package.json +++ b/package.json @@ -60,5 +60,10 @@ "*.{json,md}": [ "prettier --write" ] + }, + "jest": { + "transformModules": [ + "@kyma-project/asyncapi-react" + ] } } diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index cc7d45c139..f69e18190d 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -25,43 +25,6 @@ async function getConfig() { return require(path.resolve('jest.config.ts')); } - const options = { - rootDir: path.resolve('src'), - coverageDirectory: path.resolve('coverage'), - collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'], - moduleNameMapper: { - '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), - }, - - // We build .esm.js files with plugin:build, so to be able to load these in tests they need to be transformed - // TODO: jest is working on module support, it's possible that we can remove this in the future - transform: { - '\\.esm\\.js$': require.resolve('jest-esm-transformer'), - '\\.(js|jsx|ts|tsx)$': [ - require.resolve('ts-jest'), - { isolatedModules: true }, - ], - '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)$': require.resolve( - './jestFileTransform.js', - ), - }, - - // A bit more opinionated - testMatch: ['**/?(*.)test.{js,jsx,mjs,ts,tsx}'], - - // Default behaviour is to not apply transforms for node_modules, but we still want - // to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages. - // The @kyma-project/asyncapi-react library needs to be transformed. - transformIgnorePatterns: [ - '/node_modules/(?!@kyma-project/asyncapi-react/)(?!.*\\.(?:esm\\.js|bmp|gif|jpg|jpeg|png|frag|xml|svg)$)', - ], - }; - - // Use src/setupTests.ts as the default location for configuring test env - if (fs.existsSync('src/setupTests.ts')) { - options.setupFilesAfterEnv = ['/setupTests.ts']; - } - // We read all "jest" config fields in package.json files all the way to the filesystem root. // All configs are merged together to create the final config, with longer paths taking precedence. // The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined. @@ -92,6 +55,56 @@ async function getConfig() { currentPath = newPath; } + // We add an additional Jest config parameter only known by the Backstage CLI + // called `transformModules`. It's a list of modules that we want to apply + // our configured jest transformations for. + // This is useful when packages are published in untranspiled ESM or TS form. + const transformModules = pkgJsonConfigs + .flatMap(conf => { + const modules = conf.transformModules || []; + delete conf.transformModules; + return modules; + }) + .map(name => `${name}/`) + .join('|'); + const transformModulePattern = transformModules && `(?!${transformModules})`; + + const options = { + rootDir: path.resolve('src'), + coverageDirectory: path.resolve('coverage'), + collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'], + moduleNameMapper: { + '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), + }, + + // We build .esm.js files with plugin:build, so to be able to load these in tests they need to be transformed + // TODO: jest is working on module support, it's possible that we can remove this in the future + transform: { + '\\.esm\\.js$': require.resolve('jest-esm-transformer'), + '\\.(js|jsx|ts|tsx)$': [ + require.resolve('ts-jest'), + { isolatedModules: true }, + ], + '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)$': require.resolve( + './jestFileTransform.js', + ), + }, + + // A bit more opinionated + testMatch: ['**/?(*.)test.{js,jsx,mjs,ts,tsx}'], + + // Default behaviour is to not apply transforms for node_modules, but we still want + // to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages. + transformIgnorePatterns: [ + `/node_modules/${transformModulePattern}(?:(?!\\.esm).)*\\.(?:js|json)$`, + ], + }; + + // Use src/setupTests.ts as the default location for configuring test env + if (fs.existsSync('src/setupTests.ts')) { + options.setupFilesAfterEnv = ['/setupTests.ts']; + } + return Object.assign(options, ...pkgJsonConfigs); } diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index b59ca63fa3..af4384318a 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -42,5 +42,10 @@ "*.{json,md}": [ "prettier --write" ] + }, + "jest": { + "transformModules": [ + "@kyma-project/asyncapi-react" + ] } } From 14323967e244868f31f43c471f22d3c966253f8c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2020 06:35:43 +0000 Subject: [PATCH 15/68] chore(deps): bump inquirer from 7.2.0 to 7.3.3 Bumps [inquirer](https://github.com/SBoudrias/Inquirer.js) from 7.2.0 to 7.3.3. - [Release notes](https://github.com/SBoudrias/Inquirer.js/releases) - [Commits](https://github.com/SBoudrias/Inquirer.js/compare/inquirer@7.2.0...inquirer@7.3.3) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index 000bfa9fb9..8e24dba10d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7916,6 +7916,11 @@ cli-width@^2.0.0: resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + clipboard@^2.0.0: version "2.0.6" resolved "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376" @@ -13081,20 +13086,20 @@ inquirer@^6.2.0: through "^2.3.6" inquirer@^7.0.0, inquirer@^7.0.4: - version "7.2.0" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.2.0.tgz#63ce99d823090de7eb420e4bb05e6f3449aa389a" - integrity sha512-E0c4rPwr9ByePfNlTIB8z51kK1s2n6jrHuJeEHENl/sbq2G/S1auvibgEwNR4uSyiU+PiYHqSwsgGiXjG8p5ZQ== + version "7.3.3" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== dependencies: ansi-escapes "^4.2.1" - chalk "^3.0.0" + chalk "^4.1.0" cli-cursor "^3.1.0" - cli-width "^2.0.0" + cli-width "^3.0.0" external-editor "^3.0.3" figures "^3.0.0" - lodash "^4.17.15" + lodash "^4.17.19" mute-stream "0.0.8" run-async "^2.4.0" - rxjs "^6.5.3" + rxjs "^6.6.0" string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" @@ -20196,10 +20201,10 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.2, rxjs@^6.5.3, rxjs@^6.5.5: - version "6.6.0" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz#af2901eedf02e3a83ffa7f886240ff9018bbec84" - integrity sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg== +rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.2, rxjs@^6.5.3, rxjs@^6.5.5, rxjs@^6.6.0: + version "6.6.2" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" + integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== dependencies: tslib "^1.9.0" From d1a75c92974ecfcc426069587329fc0e851acd92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 26 Aug 2020 16:37:07 +0200 Subject: [PATCH 16/68] chore(yarn.lock): remove duplicates --- yarn.lock | 804 +++++------------------------------------------------- 1 file changed, 62 insertions(+), 742 deletions(-) diff --git a/yarn.lock b/yarn.lock index 000bfa9fb9..0c7305b955 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,30 +102,21 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/code-frame@7.8.3", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3": +"@babel/code-frame@7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== dependencies: "@babel/highlight" "^7.8.3" -"@babel/code-frame@^7.10.4": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.5.5": version "7.10.4" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== dependencies: "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.10.5.tgz#d38425e67ea96b1480a3f50404d1bf85676301a6" - integrity sha512-mPVoWNzIpYJHbWje0if7Ck36bpbtTvIxOi9+6WSK9wjGEXearAqlwBoTQvVjsAY2VIwgcs8V940geY3okzRCEw== - dependencies: - browserslist "^4.12.0" - invariant "^2.2.4" - semver "^5.5.0" - -"@babel/compat-data@^7.11.0": +"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== @@ -134,29 +125,7 @@ invariant "^2.2.4" semver "^5.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.4.4", "@babel/core@^7.4.5", "@babel/core@^7.7.5": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.9.6.tgz#d9aa1f580abf3b2286ef40b6904d390904c63376" - integrity sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.6" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helpers" "^7.9.6" - "@babel/parser" "^7.9.6" - "@babel/template" "^7.8.6" - "@babel/traverse" "^7.9.6" - "@babel/types" "^7.9.6" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.13" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/core@^7.9.0": +"@babel/core@^7.1.0", "@babel/core@^7.4.4", "@babel/core@^7.4.5", "@babel/core@^7.7.5", "@babel/core@^7.9.0": version "7.11.1" resolved "https://registry.npmjs.org/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== @@ -178,15 +147,6 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.10.5": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" - integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig== - dependencies: - "@babel/types" "^7.10.5" - jsesc "^2.5.1" - source-map "^0.5.0" - "@babel/generator@^7.11.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" @@ -196,16 +156,6 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz#5408c82ac5de98cda0d77d8124e99fa1f2170a43" - integrity sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ== - dependencies: - "@babel/types" "^7.9.6" - jsesc "^2.5.1" - lodash "^4.17.13" - source-map "^0.5.0" - "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" @@ -213,13 +163,6 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-annotate-as-pure@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" - integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" @@ -268,18 +211,6 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" -"@babel/helper-create-class-features-plugin@^7.8.3": - version "7.8.6" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.6.tgz#243a5b46e2f8f0f674dc1387631eb6b28b851de0" - integrity sha512-klTBDdsr+VFFqaDHm5rR69OpEQtO2Qv8ECxHS1mNhJJvaHArR6a1xTf5K/eZW7eZpJbhCx3NW1Yt/sKsLXLblg== - dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/helper-create-regexp-features-plugin@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" @@ -289,15 +220,6 @@ "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.0" -"@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": - version "7.8.8" - resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" - integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-regex" "^7.8.3" - regexpu-core "^4.7.0" - "@babel/helper-define-map@^7.10.4": version "7.10.5" resolved "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" @@ -324,15 +246,6 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-function-name@^7.8.3", "@babel/helper-function-name@^7.9.5": - version "7.9.5" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c" - integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw== - dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/types" "^7.9.5" - "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" @@ -340,13 +253,6 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-get-function-arity@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" - integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-hoist-variables@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" @@ -361,41 +267,14 @@ dependencies: "@babel/types" "^7.10.5" -"@babel/helper-member-expression-to-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c" - integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498" - integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-module-imports@^7.10.4": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== dependencies: "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.5.tgz#120c271c0b3353673fcdfd8c053db3c544a260d6" - integrity sha512-4P+CWMJ6/j1W915ITJaUkadLObmCRRSC234uctJfn/vHrsLNxsR8dwlcXv9ZhJWzl77awf+mWXSZEKt5t0OnlA== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" - "@babel/helper-simple-access" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.5" - lodash "^4.17.19" - -"@babel/helper-module-transforms@^7.11.0": +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== @@ -408,19 +287,6 @@ "@babel/types" "^7.11.0" lodash "^4.17.19" -"@babel/helper-module-transforms@^7.9.0": - version "7.9.0" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5" - integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA== - dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-simple-access" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/template" "^7.8.6" - "@babel/types" "^7.9.0" - lodash "^4.17.13" - "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" @@ -428,19 +294,7 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-optimise-call-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9" - integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" - integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== - -"@babel/helper-plugin-utils@^7.10.4": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== @@ -452,13 +306,6 @@ dependencies: lodash "^4.17.19" -"@babel/helper-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965" - integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ== - dependencies: - lodash "^4.17.13" - "@babel/helper-remap-async-to-generator@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz#fce8bea4e9690bbe923056ded21e54b4e8b68ed5" @@ -480,16 +327,6 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-replace-supers@^7.8.6": - version "7.8.6" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" - integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/traverse" "^7.8.6" - "@babel/types" "^7.8.6" - "@babel/helper-simple-access@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" @@ -498,14 +335,6 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-simple-access@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae" - integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw== - dependencies: - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helper-skip-transparent-expression-wrappers@^7.11.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" @@ -513,37 +342,18 @@ dependencies: "@babel/types" "^7.11.0" -"@babel/helper-split-export-declaration@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1" - integrity sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-split-export-declaration@^7.11.0": +"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== dependencies: "@babel/types" "^7.11.0" -"@babel/helper-split-export-declaration@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" - integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== -"@babel/helper-validator-identifier@^7.9.0", "@babel/helper-validator-identifier@^7.9.5": - version "7.9.5" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" - integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== - "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -563,25 +373,7 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helpers@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.6.tgz#092c774743471d0bb6c7de3ad465ab3d3486d580" - integrity sha512-tI4bUbldloLcHWoRUMAj4g1bF313M/o6fBKhIsb3QnGVPwRm9JsNf/gqMkQ7zjqReABiffPV6RWj7hEglID5Iw== - dependencies: - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.9.6" - "@babel/types" "^7.9.6" - -"@babel/highlight@^7.0.0", "@babel/highlight@^7.8.3": - version "7.9.0" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" - integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ== - dependencies: - "@babel/helper-validator-identifier" "^7.9.0" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.10.4": +"@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4", "@babel/highlight@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== @@ -590,17 +382,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz#3b1bbb30dabe600cd72db58720998376ff653bc7" - integrity sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q== - -"@babel/parser@^7.10.4", "@babel/parser@^7.10.5": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" - integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== - -"@babel/parser@^7.11.0", "@babel/parser@^7.11.1": +"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.7.5": version "7.11.3" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9" integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA== @@ -614,7 +396,7 @@ "@babel/helper-remap-async-to-generator" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-class-properties@^7.10.4", "@babel/plugin-proposal-class-properties@^7.8.3": +"@babel/plugin-proposal-class-properties@^7.10.4", "@babel/plugin-proposal-class-properties@^7.7.0", "@babel/plugin-proposal-class-properties@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== @@ -622,14 +404,6 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-class-properties@^7.7.0": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz#5e06654af5cd04b608915aada9b2a6788004464e" - integrity sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-proposal-dynamic-import@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" @@ -678,16 +452,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.4.tgz#50129ac216b9a6a55b3853fdd923e74bf553a4c0" - integrity sha512-6vh4SqRuLLarjgeOf4EaROJAHjvu9Gl+/346PbDH9yWbJyfnJ/ah3jmYKYtswEyCoWZiidvVHjHshd4WgjB9BA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.11.0", "@babel/plugin-proposal-object-rest-spread@^7.9.0": +"@babel/plugin-proposal-object-rest-spread@^7.11.0", "@babel/plugin-proposal-object-rest-spread@^7.6.2", "@babel/plugin-proposal-object-rest-spread@^7.9.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== @@ -696,14 +461,6 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-transform-parameters" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.6.2": - version "7.9.0" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f" - integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-proposal-optional-catch-binding@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" @@ -712,14 +469,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.4.tgz#750f1255e930a1f82d8cdde45031f81a0d0adff7" - integrity sha512-ZIhQIEeavTgouyMSdZRap4VPPHqJJ3NEs2cuHs5p0erH+iz6khB0qfgU8g7UuJkG88+fBMy23ZiU+nuHvekJeQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-proposal-optional-chaining@^7.11.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" @@ -737,7 +486,7 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.10.4": +"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== @@ -745,14 +494,6 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.8.8" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d" - integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.8" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -767,20 +508,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.10.4": +"@babel/plugin-syntax-class-properties@^7.10.4", "@babel/plugin-syntax-class-properties@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-class-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7" - integrity sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-dynamic-import@^7.2.0", "@babel/plugin-syntax-dynamic-import@^7.8.0": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" @@ -816,20 +550,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897" - integrity sha512-Zpg2Sgc++37kuFl6ppq2Q7Awc6E6AIW671x5PY8E/f7MCIyPPGK/EoeZXvvY3P42exZ3Q4/t3YOzP/HiN79jDg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" @@ -837,20 +564,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4": +"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-numeric-separator@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f" - integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" @@ -937,7 +657,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-dotall-regex@^7.10.4": +"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== @@ -945,14 +665,6 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" - integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-duplicate-keys@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" @@ -1014,7 +726,7 @@ "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.10.4": +"@babel/plugin-transform-modules-commonjs@^7.10.4", "@babel/plugin-transform-modules-commonjs@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== @@ -1024,16 +736,6 @@ "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.4.4": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.6.tgz#64b7474a4279ee588cacd1906695ca721687c277" - integrity sha512-7H25fSlLcn+iYimmsNe3uK1at79IE6SKW9q0/QeEHTMC9MdOZ+4bA+T1VFB5fgOqBWoqlifXRzYD0JPdmIrgSQ== - dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-simple-access" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.3" - "@babel/plugin-transform-modules-systemjs@^7.10.4": version "7.10.5" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" @@ -1167,13 +869,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-spread@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.4.tgz#4e2c85ea0d6abaee1b24dcfbbae426fe8d674cff" - integrity sha512-1e/51G/Ni+7uH5gktbWv+eCED9pP8ZpRhZB3jOaI3mmzfvJTWHkuyYTv0Z5PYtyM+Tr2Ccr9kUdQxn60fI5WuQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-transform-spread@^7.11.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" @@ -1228,77 +923,7 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" -"@babel/preset-env@^7.4.5", "@babel/preset-env@^7.9.5": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.10.4.tgz#fbf57f9a803afd97f4f32e4f798bb62e4b2bef5f" - integrity sha512-tcmuQ6vupfMZPrLrc38d0sF2OjLT3/bZ0dry5HchNCQbrokoQi4reXqclvkkAT5b+gWc23meVWpve5P/7+w/zw== - dependencies: - "@babel/compat-data" "^7.10.4" - "@babel/helper-compilation-targets" "^7.10.4" - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-proposal-async-generator-functions" "^7.10.4" - "@babel/plugin-proposal-class-properties" "^7.10.4" - "@babel/plugin-proposal-dynamic-import" "^7.10.4" - "@babel/plugin-proposal-json-strings" "^7.10.4" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" - "@babel/plugin-proposal-numeric-separator" "^7.10.4" - "@babel/plugin-proposal-object-rest-spread" "^7.10.4" - "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" - "@babel/plugin-proposal-optional-chaining" "^7.10.4" - "@babel/plugin-proposal-private-methods" "^7.10.4" - "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" - "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-class-properties" "^7.10.4" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" - "@babel/plugin-syntax-json-strings" "^7.8.0" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.10.4" - "@babel/plugin-transform-arrow-functions" "^7.10.4" - "@babel/plugin-transform-async-to-generator" "^7.10.4" - "@babel/plugin-transform-block-scoped-functions" "^7.10.4" - "@babel/plugin-transform-block-scoping" "^7.10.4" - "@babel/plugin-transform-classes" "^7.10.4" - "@babel/plugin-transform-computed-properties" "^7.10.4" - "@babel/plugin-transform-destructuring" "^7.10.4" - "@babel/plugin-transform-dotall-regex" "^7.10.4" - "@babel/plugin-transform-duplicate-keys" "^7.10.4" - "@babel/plugin-transform-exponentiation-operator" "^7.10.4" - "@babel/plugin-transform-for-of" "^7.10.4" - "@babel/plugin-transform-function-name" "^7.10.4" - "@babel/plugin-transform-literals" "^7.10.4" - "@babel/plugin-transform-member-expression-literals" "^7.10.4" - "@babel/plugin-transform-modules-amd" "^7.10.4" - "@babel/plugin-transform-modules-commonjs" "^7.10.4" - "@babel/plugin-transform-modules-systemjs" "^7.10.4" - "@babel/plugin-transform-modules-umd" "^7.10.4" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" - "@babel/plugin-transform-new-target" "^7.10.4" - "@babel/plugin-transform-object-super" "^7.10.4" - "@babel/plugin-transform-parameters" "^7.10.4" - "@babel/plugin-transform-property-literals" "^7.10.4" - "@babel/plugin-transform-regenerator" "^7.10.4" - "@babel/plugin-transform-reserved-words" "^7.10.4" - "@babel/plugin-transform-shorthand-properties" "^7.10.4" - "@babel/plugin-transform-spread" "^7.10.4" - "@babel/plugin-transform-sticky-regex" "^7.10.4" - "@babel/plugin-transform-template-literals" "^7.10.4" - "@babel/plugin-transform-typeof-symbol" "^7.10.4" - "@babel/plugin-transform-unicode-escapes" "^7.10.4" - "@babel/plugin-transform-unicode-regex" "^7.10.4" - "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.10.4" - browserslist "^4.12.0" - core-js-compat "^3.6.2" - invariant "^2.2.2" - levenary "^1.1.1" - semver "^5.5.0" - -"@babel/preset-env@^7.9.0": +"@babel/preset-env@^7.4.5", "@babel/preset-env@^7.9.0", "@babel/preset-env@^7.9.5": version "7.11.0" resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== @@ -1415,7 +1040,7 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime-corejs2@^7.10.4": +"@babel/runtime-corejs2@^7.10.4", "@babel/runtime-corejs2@^7.8.7": version "7.11.2" resolved "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.11.2.tgz#700a03945ebad0d31ba6690fc8a6bcc9040faa47" integrity sha512-AC/ciV28adSSpEkBglONBWq4/Lvm6GAZuxIoyVtsnUpZMl0bxLtoChEnYAkP+47KyOCayZanojtflUEUJtR/6Q== @@ -1423,15 +1048,7 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" -"@babel/runtime-corejs2@^7.8.7": - version "7.10.3" - resolved "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.10.3.tgz#81bc99a96bfcb6db3f0dcf73fdc577cc554d341b" - integrity sha512-enKvnR/kKFbZFgXYo165wtSA5OeiTlgsnU4jV3vpKRhfWUJjLS6dfVcjIPeRcgJbgEgdgu0I+UyBWqu6c0GumQ== - dependencies: - core-js "^2.6.5" - regenerator-runtime "^0.13.4" - -"@babel/runtime-corejs3@^7.10.2": +"@babel/runtime-corejs3@^7.10.2", "@babel/runtime-corejs3@^7.8.3": version "7.10.3" resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz#931ed6941d3954924a7aa967ee440e60c507b91a" integrity sha512-HA7RPj5xvJxQl429r5Cxr2trJwOfPjKiqhCXcdQPSqO2G0RHPZpXu4fkYmBaTKCp2c/jRaMK9GB/lN+7zvvFPw== @@ -1439,14 +1056,6 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime-corejs3@^7.8.3": - version "7.9.2" - resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.2.tgz#26fe4aa77e9f1ecef9b776559bbb8e84d34284b7" - integrity sha512-HHxmgxbIzOfFlZ+tdeRKtaxWOMUoCG5Mu3wKeUmOxjYrwb3AAHgnmtCUbPPK11/raIWLIBK250t8E2BPO0p7jA== - dependencies: - core-js-pure "^3.0.0" - regenerator-runtime "^0.13.4" - "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.10.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.11.2" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" @@ -1454,7 +1063,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.10.4": +"@babel/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.7.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== @@ -1463,46 +1072,7 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/template@^7.3.3", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": - version "7.8.6" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" - integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/parser" "^7.8.6" - "@babel/types" "^7.8.6" - -"@babel/traverse@^7.1.0", "@babel/traverse@^7.7.4", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz#5540d7577697bf619cc57b92aa0f1c231a94f442" - integrity sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.6" - "@babel/helper-function-name" "^7.9.5" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.6" - "@babel/types" "^7.9.6" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/traverse@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" - integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.10.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.10.4" - "@babel/parser" "^7.10.5" - "@babel/types" "^7.10.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/traverse@^7.11.0", "@babel/traverse@^7.9.0": +"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.7.4", "@babel/traverse@^7.9.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== @@ -1517,25 +1087,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5", "@babel/types@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz#2c5502b427251e9de1bd2dff95add646d95cc9f7" - integrity sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA== - dependencies: - "@babel/helper-validator-identifier" "^7.9.5" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@babel/types@^7.10.4", "@babel/types@^7.10.5": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15" - integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" - to-fast-properties "^2.0.0" - -"@babel/types@^7.11.0": +"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.9.0", "@babel/types@^7.9.5": version "7.11.0" resolved "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== @@ -3036,15 +2588,6 @@ before-after-hook "^2.1.0" universal-user-agent "^5.0.0" -"@octokit/endpoint@^5.5.0": - version "5.5.3" - resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.3.tgz#0397d1baaca687a4c8454ba424a627699d97c978" - integrity sha512-EzKwkwcxeegYYah5ukEeAI/gYRLv2Y9U5PpIsseGSFDk+G3RbipQGBs8GuYS1TLCtQaqoO66+aQGtITPalxsNQ== - dependencies: - "@octokit/types" "^2.0.0" - is-plain-object "^3.0.0" - universal-user-agent "^5.0.0" - "@octokit/endpoint@^6.0.1": version "6.0.3" resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.3.tgz#dd09b599662d7e1b66374a177ab620d8cdf73487" @@ -3103,7 +2646,7 @@ "@octokit/types" "^5.0.0" deprecation "^2.3.1" -"@octokit/request-error@^1.0.1", "@octokit/request-error@^1.0.2": +"@octokit/request-error@^1.0.2": version "1.2.1" resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA== @@ -3121,21 +2664,7 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.2.0": - version "5.3.2" - resolved "https://registry.npmjs.org/@octokit/request/-/request-5.3.2.tgz#1ca8b90a407772a1ee1ab758e7e0aced213b9883" - integrity sha512-7NPJpg19wVQy1cs2xqXjjRq/RmtSomja/VSWnptfYwuBxLdbYh2UjhGi0Wx7B1v5Iw5GKhfFDQL7jM7SSp7K2g== - dependencies: - "@octokit/endpoint" "^5.5.0" - "@octokit/request-error" "^1.0.1" - "@octokit/types" "^2.0.0" - deprecation "^2.0.0" - is-plain-object "^3.0.0" - node-fetch "^2.3.0" - once "^1.4.0" - universal-user-agent "^5.0.0" - -"@octokit/request@^5.3.0", "@octokit/request@^5.4.0": +"@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.4.0": version "5.4.5" resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.5.tgz#8df65bd812047521f7e9db6ff118c06ba84ac10b" integrity sha512-atAs5GAGbZedvJXXdjtKljin+e2SltEs48B3naJjqWupYl2IUBbB/CJisyjbNHcKpHzb3E+OYEZ46G8eakXgQg== @@ -3266,17 +2795,7 @@ resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@reach/router@^1.2.1": - version "1.3.3" - resolved "https://registry.npmjs.org/@reach/router/-/router-1.3.3.tgz#58162860dce6c9449d49be86b0561b5ef46d80db" - integrity sha512-gOIAiFhWdiVGSVjukKeNKkCRBLmnORoTPyBihI/jLunICPgxdP30DroAvPQuf1eVfQbfGJQDJkwhJXsNPMnVWw== - dependencies: - create-react-context "0.3.0" - invariant "^2.2.3" - prop-types "^15.6.1" - react-lifecycles-compat "^3.0.4" - -"@reach/router@^1.3.3": +"@reach/router@^1.2.1", "@reach/router@^1.3.3": version "1.3.4" resolved "https://registry.npmjs.org/@reach/router/-/router-1.3.4.tgz#d2574b19370a70c80480ed91f3da840136d10f8c" integrity sha512-+mtn9wjlB9NN2CNnnC/BRYtwdKBfSyyasPYraNAyvaV1occr/5NnB4CVzjEZipNHwYebQwcndGUmpFzxAUoqSA== @@ -5071,15 +4590,7 @@ resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== -"@types/reach__router@^1.2.3": - version "1.3.1" - resolved "https://registry.npmjs.org/@types/reach__router/-/reach__router-1.3.1.tgz#ca8b431acb12bb897d2b806f6fdd815f056d6d02" - integrity sha512-E51ntVeunnxofXmOoPFiOvElHWf+jBEs3B56gGx7XhPHOkJdjWxWDY4V1AsUiwhtOCXPM7atFy30wj7glyv2Fg== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/reach__router@^1.3.5": +"@types/reach__router@^1.2.3", "@types/reach__router@^1.3.5": version "1.3.5" resolved "https://registry.npmjs.org/@types/reach__router/-/reach__router-1.3.5.tgz#14e1e981cccd3a5e50dc9e969a72de0b9d472f6d" integrity sha512-h0NbqXN/tJuBY/xggZSej1SKQEstbHO7J/omt1tYoFGmj3YXOodZKbbqD4mNDh7zvEGYd7YFrac1LTtAr3xsYQ== @@ -5768,12 +5279,7 @@ acorn@^6.0.1, acorn@^6.4.1: resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" - integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== - -acorn@^7.2.0: +acorn@^7.1.1, acorn@^7.2.0: version "7.3.1" resolved "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== @@ -5865,17 +5371,7 @@ ajv@^5.0.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.7.0: - version "6.12.2" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" - integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^6.10.1: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.7.0: version "6.12.3" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== @@ -6493,20 +5989,7 @@ autolinker@~0.28.0: dependencies: gulp-header "^1.7.1" -autoprefixer@^9.7.2: - version "9.7.4" - resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.4.tgz#f8bf3e06707d047f0641d87aee8cfb174b2a5378" - integrity sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g== - dependencies: - browserslist "^4.8.3" - caniuse-lite "^1.0.30001020" - chalk "^2.4.2" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^7.0.26" - postcss-value-parser "^4.0.2" - -autoprefixer@^9.7.5: +autoprefixer@^9.7.2, autoprefixer@^9.7.5: version "9.8.6" resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== @@ -7217,7 +6700,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@4.10.0, browserslist@^4.0.0, browserslist@^4.8.3: +browserslist@4.10.0: version "4.10.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.10.0.tgz#f179737913eaf0d2b98e4926ac1ca6a15cbcc6a9" integrity sha512-TpfK0TDgv71dzuTsEAlQiHeWQ/tiPqgNZVdv046fvNtBZrjbv2O3TsWCDU0AWGJJKCF/KsjNdLzR9hXOsh/CfA== @@ -7236,7 +6719,7 @@ browserslist@4.7.0: electron-to-chromium "^1.3.247" node-releases "^1.1.29" -browserslist@^4.12.0: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.3: version "4.13.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d" integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ== @@ -7602,17 +7085,7 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001035: - version "1.0.30001035" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001035.tgz#2bb53b8aa4716b2ed08e088d4dc816a5fe089a1e" - integrity sha512-C1ZxgkuA4/bUEdMbU5WrGY4+UhMFFiXrgNAfxiMIqWgFTWfv/xsZCS2xEHT2LMq7xAZfuAnu6mcqyDl0ZR6wLQ== - -caniuse-lite@^1.0.30001093: - version "1.0.30001107" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001107.tgz#809360df7a5b3458f627aa46b0f6ed6d5239da9a" - integrity sha512-86rCH+G8onCmdN4VZzJet5uPELII59cUzDphko3thQFgAQG1RNa+sVLDoALIhRYmflo5iSIzWY3vu1XTWtNMQQ== - -caniuse-lite@^1.0.30001109: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001109: version "1.0.30001113" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001113.tgz#22016ab55b5a8b04fa00ca342d9ee1b98df48065" integrity sha512-qMvjHiKH21zzM/VDZr6oosO6Ri3U0V2tC015jRXjOecwQCJtsU5zklTNTk31jQbIOP8gha0h1ccM/g0ECP+4BA== @@ -8599,21 +8072,13 @@ cross-env@^7.0.0: dependencies: cross-spawn "^7.0.1" -cross-fetch@3.0.5, cross-fetch@^3.0.5: +cross-fetch@3.0.5, cross-fetch@^3.0.4, cross-fetch@^3.0.5: version "3.0.5" resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.5.tgz#2739d2981892e7ab488a7ad03b92df2816e03f4c" integrity sha512-FFLcLtraisj5eteosnX1gf01qYDCOc4fDy0+euOt8Kn9YBY2NtXL/pCoYPavw24NIQkQqm5ZOLsGD5Zzj0gyew== dependencies: node-fetch "2.6.0" -cross-fetch@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.4.tgz#7bef7020207e684a7638ef5f2f698e24d9eb283c" - integrity sha512-MSHgpjQqgbT/94D4CyADeNoYh52zMkCX4pcJvPP5WqPsLFMKjr2TCMg381ox5qI0ii2dPwaLx/00477knXqXVw== - dependencies: - node-fetch "2.6.0" - whatwg-fetch "3.0.0" - cross-spawn@6.0.5, cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -9657,7 +9122,7 @@ dom-converter@^0.2: dependencies: utila "~0.4" -dom-helpers@^5.0.0: +dom-helpers@^5.0.0, dom-helpers@^5.0.1: version "5.1.4" resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b" integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A== @@ -9665,14 +9130,6 @@ dom-helpers@^5.0.0: "@babel/runtime" "^7.8.7" csstype "^2.6.7" -dom-helpers@^5.0.1: - version "5.1.3" - resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" - integrity sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw== - dependencies: - "@babel/runtime" "^7.6.3" - csstype "^2.6.7" - dom-serializer@0, dom-serializer@^0.2.1: version "0.2.2" resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -9903,12 +9360,7 @@ ejs@^2.7.4: resolved "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== -electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378: - version "1.3.380" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.380.tgz#1e1f07091b42b54bccd0ad6d3a14f2b73b60dc9d" - integrity sha512-2jhQxJKcjcSpVOQm0NAfuLq8o+130blrcawoumdXT6411xG/xIAOyZodO/y7WTaYlz/NHe3sCCAe/cJLnDsqTw== - -electron-to-chromium@^1.3.488: +electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.488: version "1.3.509" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.509.tgz#830fcb89cd66dc2984d18d794973b99e3f00584c" integrity sha512-cN4lkjNRuTG8rtAqTOVgwpecEC2kbKA04PG6YijcKGHK/kD0xLjiqExcAOmLUwtXZRF8cBeam2I0VZcih919Ug== @@ -10209,19 +9661,7 @@ escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escodegen@^1.14.1, escodegen@^1.9.1: - version "1.14.1" - resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" - integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== - dependencies: - esprima "^4.0.1" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -escodegen@^1.6.1: +escodegen@^1.14.1, escodegen@^1.6.1, escodegen@^1.9.1: version "1.14.3" resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== @@ -11721,14 +11161,7 @@ git-up@^4.0.0: is-ssh "^1.3.0" parse-url "^5.0.0" -git-url-parse@^11.1.2: - version "11.1.2" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.1.2.tgz#aff1a897c36cc93699270587bea3dbcbbb95de67" - integrity sha512-gZeLVGY8QVKMIkckncX+iCq2/L8PlwncvDFKiWkBn9EtCfYDbliRTTp6qzyQ1VMdITUfq7293zDzfpjdiGASSQ== - dependencies: - git-up "^4.0.0" - -git-url-parse@^11.1.3: +git-url-parse@^11.1.2, git-url-parse@^11.1.3: version "11.1.3" resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.1.3.tgz#03625b6fc09905e9ad1da7bb2b84be1bf9123143" integrity sha512-GPsfwticcu52WQ+eHp0IYkAyaOASgYdtsQDIt4rUp6GbiNt1P9ddrh3O0kQB0eD4UJZszVqNT3+9Zwcg40fywA== @@ -12670,16 +12103,7 @@ http-proxy-middleware@^0.19.1: lodash "^4.17.11" micromatch "^3.1.10" -http-proxy@^1.17.0: - version "1.18.0" - resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" - integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http-proxy@^1.18.1: +http-proxy@^1.17.0, http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== @@ -13368,12 +12792,7 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-function@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" - integrity sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU= - -is-function@^1.0.2: +is-function@^1.0.1, is-function@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08" integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ== @@ -13558,16 +12977,11 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= -is-promise@^2.1: +is-promise@^2.1, is-promise@^2.1.0: version "2.2.2" resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== -is-promise@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= - is-reference@^1.1.2: version "1.1.4" resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz#3f95849886ddb70256a3e6d062b1a68c13c51427" @@ -13575,14 +12989,7 @@ is-reference@^1.1.2: dependencies: "@types/estree" "0.0.39" -is-regex@^1.0.4, is-regex@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" - integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== - dependencies: - has "^1.0.3" - -is-regex@^1.1.1: +is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== @@ -13994,12 +13401,7 @@ jest-fetch-mock@^3.0.3: cross-fetch "^3.0.4" promise-polyfill "^8.1.3" -jest-get-type@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876" - integrity sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw== - -jest-get-type@^25.2.6: +jest-get-type@^25.1.0, jest-get-type@^25.2.6: version "25.2.6" resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== @@ -14320,15 +13722,7 @@ js-tokens@^3.0.2: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.13.1, js-yaml@^3.8.3: - version "3.13.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^3.14.0, js-yaml@^3.8.1: +js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.8.1, js-yaml@^3.8.3: version "3.14.0" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== @@ -15272,7 +14666,7 @@ lodash.without@^4.4.0: resolved "https://registry.npmjs.org/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw= -lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.1, lodash@~4.17.10: +lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.1, lodash@~4.17.10: version "4.17.20" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -15835,12 +15229,7 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.43.0, "mime-db@>= 1.43.0 < 2": - version "1.43.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" - integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== - -mime-db@1.44.0, mime-db@^1.28.0: +mime-db@1.44.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: version "1.44.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== @@ -15857,14 +15246,7 @@ mime-types@2.1.18: dependencies: mime-db "~1.33.0" -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.26" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" - integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== - dependencies: - mime-db "1.43.0" - -mime-types@^2.1.26: +mime-types@^2.1.12, mime-types@^2.1.26, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -16201,12 +15583,7 @@ mz@^2.5.0, mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.12.1: - version "2.14.0" - resolved "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== - -nan@^2.14.0: +nan@^2.12.1, nan@^2.14.0: version "2.14.1" resolved "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== @@ -16447,14 +15824,7 @@ node-pre-gyp@^0.13.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.29, node-releases@^1.1.52: - version "1.1.52" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9" - integrity sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ== - dependencies: - semver "^6.3.0" - -node-releases@^1.1.58: +node-releases@^1.1.29, node-releases@^1.1.52, node-releases@^1.1.58: version "1.1.60" resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== @@ -17872,14 +17242,7 @@ pnp-webpack-plugin@1.5.0: dependencies: ts-pnp "^1.1.2" -polished@^3.3.1: - version "3.5.0" - resolved "https://registry.npmjs.org/polished/-/polished-3.5.0.tgz#bffb0db79025c61d1f735f9bb77a379f5fc46345" - integrity sha512-TujkqjczBuuG8ObaNeq+zCCu46tTdaWxqtMxCGTxsV5NYTr9pL08H1P0jgy1V4PXLWm2UlIj+icSEwAZv7I8TA== - dependencies: - "@babel/runtime" "^7.8.7" - -polished@^3.4.4: +polished@^3.3.1, polished@^3.4.4: version "3.6.5" resolved "https://registry.npmjs.org/polished/-/polished-3.6.5.tgz#dbefdde64c675935ec55119fe2a2ab627ca82e9c" integrity sha512-VwhC9MlhW7O5dg/z7k32dabcAFW1VI2+7fSe8cE/kXcfL7mVdoa5UxciYGW2sJU78ldDLT6+ROEKIZKFNTnUXQ== @@ -17891,7 +17254,7 @@ popper.js@^1.14.1, popper.js@^1.14.4, popper.js@^1.14.7: resolved "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== -portfinder@^1.0.25: +portfinder@^1.0.25, portfinder@^1.0.26: version "1.0.28" resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== @@ -17900,15 +17263,6 @@ portfinder@^1.0.25: debug "^3.1.1" mkdirp "^0.5.5" -portfinder@^1.0.26: - version "1.0.26" - resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" - integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.1" - posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -18421,20 +17775,13 @@ pretty-hrtime@^1.0.3: resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= -prismjs@^1.17.1, prismjs@^1.21.0, prismjs@~1.21.0: +prismjs@^1.17.1, prismjs@^1.21.0, prismjs@^1.8.4, prismjs@~1.21.0: version "1.21.0" resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3" integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw== optionalDependencies: clipboard "^2.0.0" -prismjs@^1.8.4: - version "1.20.0" - resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03" - integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ== - optionalDependencies: - clipboard "^2.0.0" - prismjs@~1.17.0: version "1.17.1" resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.17.1.tgz#e669fcbd4cdd873c35102881c33b14d0d68519be" @@ -18689,16 +18036,11 @@ qs@6.7.0: resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.4.0, qs@^6.9.4: +qs@^6.4.0, qs@^6.5.1, qs@^6.6.0, qs@^6.9.4: version "6.9.4" resolved "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== -qs@^6.5.1, qs@^6.6.0: - version "6.9.3" - resolved "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" - integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== - qs@~6.5.2: version "6.5.2" resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -20576,16 +19918,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: - version "0.8.3" - resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" - integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -shelljs@^0.8.4: +shelljs@^0.8.3, shelljs@^0.8.4: version "0.8.4" resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== @@ -21467,15 +20800,7 @@ style-inject@^0.3.0: resolved "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz#d21c477affec91811cc82355832a700d22bf8dd3" integrity sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw== -style-loader@^1.0.0: - version "1.1.3" - resolved "https://registry.npmjs.org/style-loader/-/style-loader-1.1.3.tgz#9e826e69c683c4d9bf9db924f85e9abb30d5e200" - integrity sha512-rlkH7X/22yuwFYK357fMN/BxYOorfnfq0eD7+vqlemSK4wEcejFF1dg4zxP0euBW8NrYx2WZzZ8PPFevr7D+Kw== - dependencies: - loader-utils "^1.2.3" - schema-utils "^2.6.4" - -style-loader@^1.2.1: +style-loader@^1.0.0, style-loader@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a" integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg== @@ -21982,16 +21307,11 @@ throat@^5.0.0: resolved "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== -throttle-debounce@^2.0.1: +throttle-debounce@^2.0.1, throttle-debounce@^2.1.0: version "2.2.1" resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.2.1.tgz#fbd933ae6793448816f7d5b3cae259d464c98137" integrity sha512-i9hAVld1f+woAiyNGqWelpDD5W1tpMroL3NofTz9xzwq6acWBlO2dC8k5EFSZepU6oOINtV5Q3aSPoRg7o4+fA== -throttle-debounce@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5" - integrity sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg== - throttleit@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" @@ -23278,16 +22598,16 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" -whatwg-fetch@3.0.0, whatwg-fetch@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" - integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== - whatwg-fetch@^2.0.0, whatwg-fetch@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== +whatwg-fetch@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" + integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== + whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" From 1665dbbb46f2b08dce7da0cd4c3c61e13c3c1e1c Mon Sep 17 00:00:00 2001 From: Govindarajan Nagarajan Date: Wed, 26 Aug 2020 13:29:53 +0200 Subject: [PATCH 17/68] Update Refresh Token, if provided by the server in the refresh grant The OAuth2 specifications specify that during the refresh grant, the server may provide an alternate refresh Token along with the access Token when performing a token refresh. The oauth2 provider in the `auth-backend` ignores the newer refresh token during the refresh grant. This commit will update the refresh token cookie at the end of the call to the /auth/oauth2/refresh endpoint if the server provides a new refresh token grant a --- plugins/auth-backend/src/lib/OAuthProvider.ts | 7 +++++++ .../auth-backend/src/lib/PassportStrategyHelper.ts | 4 ++-- plugins/auth-backend/src/providers/oauth2/provider.ts | 11 +++++++---- plugins/auth-backend/src/providers/types.ts | 8 ++++++++ 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend/src/lib/OAuthProvider.ts b/plugins/auth-backend/src/lib/OAuthProvider.ts index f2b8c8e09c..8e9cd510b3 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.ts @@ -258,6 +258,13 @@ export class OAuthProvider implements AuthProviderRouteHandlers { await this.populateIdentity(response.backstageIdentity); + if ( + response.providerInfo.refreshToken && + response.providerInfo.refreshToken !== refreshToken + ) { + this.setRefreshTokenCookie(res, response.providerInfo.refreshToken); + } + res.send(response); } catch (error) { res.status(401).send(`${error.message}`); diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 4cc0de4444..9a169f9696 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -45,7 +45,6 @@ export const makeProfileInfo = ( if ((!email || !picture) && idToken) { try { const decoded: Record = jwtDecoder(idToken); - if (!email && decoded.email) { email = decoded.email; } @@ -133,7 +132,7 @@ export const executeRefreshTokenStrategy = async ( ( err: Error | null, accessToken: string, - _refreshToken: string, + newRefreshToken: string, params: any, ) => { if (err) { @@ -149,6 +148,7 @@ export const executeRefreshTokenStrategy = async ( resolve({ accessToken, + refreshToken: newRefreshToken, params, }); }, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index c03f7c4371..3a8982c680 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -55,6 +55,7 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { done: PassportDoneCallback, ) => { const profile = makeProfileInfo(rawProfile, params.id_token); + done( undefined, { @@ -101,21 +102,24 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { } async refresh(refreshToken: string, scope: string): Promise { - const { accessToken, params } = await executeRefreshTokenStrategy( + const refreshTokenResponse = await executeRefreshTokenStrategy( this._strategy, refreshToken, scope, ); + const { accessToken, params } = refreshTokenResponse; + const updatedRefreshToken = refreshTokenResponse.refreshToken; const profile = await executeFetchUserProfileStrategy( this._strategy, - accessToken, - params.id_token, + refreshTokenResponse.accessToken, + refreshTokenResponse.params.id_token, ); return this.populateIdentity({ providerInfo: { accessToken, + refreshToken: updatedRefreshToken, idToken: params.id_token, expiresInSeconds: params.expires_in, scope: params.scope, @@ -134,7 +138,6 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { if (!profile.email) { throw new Error('Profile does not contain a profile'); } - const id = profile.email.split('@')[0]; return { ...response, backstageIdentity: { id } }; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index d372fd1b94..0d4b7c656a 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -259,6 +259,10 @@ export type OAuthProviderInfo = { * Scopes granted for the access token. */ scope: string; + /** + * A refresh token issued for the signed in user + */ + refreshToken?: string; }; export type OAuthPrivateInfo = { @@ -326,6 +330,10 @@ export type RefreshTokenResponse = { * An access token issued for the signed in user. */ accessToken: string; + /** + * Optionally, the server can issue a new Refresh Token for the user + */ + refreshToken?: string; params: any; }; From 0b2f78769933196aa8ae18997c6a2847bd3c715b Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Thu, 27 Aug 2020 09:52:49 +0200 Subject: [PATCH 18/68] Updated docs for techdocs plugin (#2127) * Updated docs for techdocs plugin * Error in example comment * Update plugins/techdocs/README.md Co-authored-by: Emma Indal * Update plugins/techdocs/src/reader/README.md Co-authored-by: Emma Indal * Update plugins/techdocs/src/reader/README.md Co-authored-by: Emma Indal * Update plugins/techdocs/src/reader/README.md Co-authored-by: Emma Indal * Update link to transformers doc to point to reader doc for now Co-authored-by: Emma Indal --- docs/features/techdocs/concepts.md | 2 +- plugins/techdocs/README.md | 38 ++----------------- plugins/techdocs/src/reader/README.md | 21 +++++++++- .../src/reader/transformers/README.md | 1 - 4 files changed, 25 insertions(+), 37 deletions(-) delete mode 100644 plugins/techdocs/src/reader/transformers/README.md diff --git a/docs/features/techdocs/concepts.md b/docs/features/techdocs/concepts.md index 7d5b5325cf..2fa84cebd6 100644 --- a/docs/features/techdocs/concepts.md +++ b/docs/features/techdocs/concepts.md @@ -53,4 +53,4 @@ Reader. The reason why transformers were introduced was to provide a way to transform the HTML content on pre and post render (e.g. rewrite docs links or modify css). -[Transformers API docs](https://github.com/spotify/backstage/blob/master/plugins/techdocs/src/reader/transformers/README.md) +[Transformers API docs](https://github.com/spotify/backstage/blob/master/plugins/techdocs/src/reader/README.md) diff --git a/plugins/techdocs/README.md b/plugins/techdocs/README.md index 4a92c814a1..2e9365f6c6 100644 --- a/plugins/techdocs/README.md +++ b/plugins/techdocs/README.md @@ -1,45 +1,15 @@ # TechDocs Plugin -Welcome to the TechDocs plugin - Spotify's docs-like-code approach built directly into [Backstage](https://backstage.io). Watch [a video of our approach on YouTube](https://www.youtube.com/watch?v=uFGCaZmA6d4) to learn more. - -**WIP: This plugin is a work in progress. It is not ready for use yet. Follow our progress on [the Backstage Discord](https://discord.gg/MUpMjP2) under #docs-like-code or on [our GitHub Milestone](https://github.com/spotify/backstage/milestone/15).** - ## Getting started -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 [/docs](http://localhost:3000/docs). - -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. +Set up Backstage and TechDocs by follow our guide on [Getting Started](../../docs/features/techdocs/getting-started.md). ## Configuration ### Custom Storage URL -TechDocs currently reads a static HTML file, generated by Mkdocs (see our `packages/techdocs-container` folder for more documentation) and stored on an external server, and loads that into Backstage. By default, we have set up a mock server with some example documentation sites over in Google Cloud Storage: +TechDocs will try to read your documentation from the URL you have specified in the `techdocs storageUrl` in `app-config.yml`. -```md -# Base URL +### TechDocs Storage Api -https://techdocs-mock-sites.storage.googleapis.com - -# Home Page for the "mkdocs" docs - -https://techdocs-mock-sites.storage.googleapis.com/mkdocs/index.html - -# Home Page for the "backstage-microsite" docs - -https://techdocs-mock-sites.storage.googleapis.com/backstage-microsite/index.html -``` - -Using your own setup (or ours which is being worked on as of Q3 2020), you can point it to your own server with your own hosted documentation sites. The only requirement is that it the output is from [Mkdocs](https://mkdocs.org) with the Material theme. You can always use our documentation generation tool located at `packages/techdocs-container` for easy setup. - -To point TechDocs to your own server, simply update the `techdocs.storageUrl` value in your `app-config.yaml` file or set the environment variable `APP_CONFIG_techdocs_storageUrl` in your application: - -```bash -git clone git@github.com:spotify/backstage.git -cd backstage/ -yarn install -export APP_CONFIG_techdocs_storageUrl='"http://example-docs-site-server.com"' -yarn start -``` +The default setup of TechDocs assumes your documentation is accessed by reading a page with the format of `///`. If for some reason you want to change this it can be configured by implementing a new techdocs storage API. Do this by implementing TechDocsStorage found in `plugins/techdocs/src/api`. Add your new API to the application in `app/src/apis.ts` (or replace if it's already registered as an API). diff --git a/plugins/techdocs/src/reader/README.md b/plugins/techdocs/src/reader/README.md index 464090415c..fb9f613651 100644 --- a/plugins/techdocs/src/reader/README.md +++ b/plugins/techdocs/src/reader/README.md @@ -1 +1,20 @@ -# TODO +# TechDocs Reader + +The TechDocs reader is a component that fetches a remote page, runs transformers on it and renders it into a shadow dom. + +Currently there's no easy way to customize which transformers to run or add new ones. If that is needed you would have to fork the techdocs plugin and make your changes in that fork. + +Transformers are functions that optionally takes in parameters from the Reader.tsx component and returns a function which gets passed the DOM of the fetched page. A very simple transformer can look like this. + +```typescript +export const updateH1Text = (): Transformer => { + return dom => { + // Change the first occurance of H1 to say "TechDocs!" + dom.querySelector('h1')?.innerHTML = 'TechDocs!'; + + return dom; + }; +}; +``` + +The transformers are then registered in the Reader.tsx file. They are registered in two places, one place that runs before it's attached to the actual browser DOM (preTransformers) and once after (postTransfomers). Doing modifications is faster before it's attached, but doesn't allow us to do some things, such as attaching event listeners. diff --git a/plugins/techdocs/src/reader/transformers/README.md b/plugins/techdocs/src/reader/transformers/README.md deleted file mode 100644 index 464090415c..0000000000 --- a/plugins/techdocs/src/reader/transformers/README.md +++ /dev/null @@ -1 +0,0 @@ -# TODO From 9da9a1c94e4744d9887ffc4f54f0d234ff2871d4 Mon Sep 17 00:00:00 2001 From: Govindarajan Nagarajan Date: Thu, 27 Aug 2020 11:08:23 +0200 Subject: [PATCH 19/68] refactor: styling and variable naming --- plugins/auth-backend/src/providers/oauth2/provider.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 3a8982c680..d74e40bf64 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -107,13 +107,16 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { refreshToken, scope, ); - const { accessToken, params } = refreshTokenResponse; - const updatedRefreshToken = refreshTokenResponse.refreshToken; + const { + accessToken, + params, + refreshToken: updatedRefreshToken, + } = refreshTokenResponse; const profile = await executeFetchUserProfileStrategy( this._strategy, - refreshTokenResponse.accessToken, - refreshTokenResponse.params.id_token, + accessToken, + params.id_token, ); return this.populateIdentity({ From d51b61dbafa9b0f141ad3c5479fbb265718fe928 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 26 Aug 2020 20:11:33 +0200 Subject: [PATCH 20/68] fix(circleci): unbreak plugin --- .../components/{App.tsx => CircleCIWidget.tsx} | 18 ++---------------- plugins/circleci/src/index.ts | 2 +- .../BuildWithStepsPage/BuildWithStepsPage.tsx | 15 ++++++++++----- .../src/pages/BuildsPage/BuildsPage.tsx | 15 ++++++++++----- plugins/circleci/src/plugin.ts | 8 +++++--- plugins/circleci/src/route-refs.tsx | 7 ++++++- plugins/circleci/src/state/useBuilds.ts | 1 - 7 files changed, 34 insertions(+), 32 deletions(-) rename plugins/circleci/src/components/{App.tsx => CircleCIWidget.tsx} (73%) diff --git a/plugins/circleci/src/components/App.tsx b/plugins/circleci/src/components/CircleCIWidget.tsx similarity index 73% rename from plugins/circleci/src/components/App.tsx rename to plugins/circleci/src/components/CircleCIWidget.tsx index a3f7112132..767eb6d5c7 100644 --- a/plugins/circleci/src/components/App.tsx +++ b/plugins/circleci/src/components/CircleCIWidget.tsx @@ -15,25 +15,11 @@ */ import React from 'react'; import { Route, MemoryRouter, Routes } from 'react-router'; -import { BuildsPage, Builds } from '../pages/BuildsPage'; -import { DetailedViewPage, BuildWithSteps } from '../pages/BuildWithStepsPage'; +import { Builds } from '../pages/BuildsPage'; +import { BuildWithSteps } from '../pages/BuildWithStepsPage'; import { AppStateProvider } from '../state'; import { Settings } from './Settings'; -export const App = () => { - return ( - - <> - - } /> - } /> - - - - - ); -}; - // TODO: allow pass in settings as props // When some shared settings workflow // will be established diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts index 11f2c80b88..e2e6c4fa69 100644 --- a/plugins/circleci/src/index.ts +++ b/plugins/circleci/src/index.ts @@ -17,4 +17,4 @@ export { plugin } from './plugin'; export * from './api'; export * from './route-refs'; -export { CircleCIWidget } from './components/App'; +export { CircleCIWidget } from './components/CircleCIWidget'; diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx index 77e6daf755..d0e0c655c3 100644 --- a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -25,6 +25,8 @@ import { Layout } from '../../components/Layout'; import LaunchIcon from '@material-ui/icons/Launch'; import { useSettings } from '../../state/useSettings'; import { useBuildWithSteps } from '../../state/useBuildWithSteps'; +import { AppStateProvider } from '../../state'; +import { Settings } from '../../components/Settings'; const IconLink = IconButton as typeof Link; const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => ( @@ -93,11 +95,14 @@ const pickClassName = ( }; const Page = () => ( - - - - - + + + + + + + + ); const BuildWithStepsView: FC<{}> = () => { diff --git a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx index 7b124c03c1..6e7fc44249 100644 --- a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx +++ b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx @@ -19,13 +19,18 @@ import { Grid } from '@material-ui/core'; import { Builds as BuildsComp } from './lib/Builds'; import { Layout } from '../../components/Layout'; import { PluginHeader } from '../../components/PluginHeader'; +import { AppStateProvider } from '../../state/AppState'; +import { Settings } from '../../components/Settings'; const BuildsPage: FC<{}> = () => ( - - - - - + + + + + + + + ); const Builds = () => ( diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 6a2e37df10..47ecccef4f 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -14,12 +14,14 @@ * limitations under the License. */ import { createPlugin } from '@backstage/core'; -import { App } from './components/App'; -import { circleCIRouteRef } from './route-refs'; +import { circleCIRouteRef, circleCIBuildRouteRef } from './route-refs'; +import BuildsPage from './pages/BuildsPage/BuildsPage'; +import BuildWithStepsPage from './pages/BuildWithStepsPage/BuildWithStepsPage'; export const plugin = createPlugin({ id: 'circleci', register({ router }) { - router.addRoute(circleCIRouteRef, App, { exact: false }); + router.addRoute(circleCIRouteRef, BuildsPage); + router.addRoute(circleCIBuildRouteRef, BuildWithStepsPage); }, }); diff --git a/plugins/circleci/src/route-refs.tsx b/plugins/circleci/src/route-refs.tsx index d87ad2fbad..581f035c1d 100644 --- a/plugins/circleci/src/route-refs.tsx +++ b/plugins/circleci/src/route-refs.tsx @@ -34,5 +34,10 @@ const CircleCIIcon: FC = props => ( export const circleCIRouteRef = createRouteRef({ icon: CircleCIIcon, path: '/circleci', - title: 'CircleCI', + title: 'CircleCI | All builds', +}); + +export const circleCIBuildRouteRef = createRouteRef({ + path: '/circleci/build/:buildId', + title: 'CircleCI | Build info', }); diff --git a/plugins/circleci/src/state/useBuilds.ts b/plugins/circleci/src/state/useBuilds.ts index 6d38c5f901..282ef68f12 100644 --- a/plugins/circleci/src/state/useBuilds.ts +++ b/plugins/circleci/src/state/useBuilds.ts @@ -70,7 +70,6 @@ export const transform = ( export function useBuilds() { const [{ repo, owner, token }] = useSettings(); - const api = useApi(circleCIApiRef); const errorApi = useApi(errorApiRef); From 23c34ff0b149601371d09d980ed9275218b347e4 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2020 11:05:18 +0000 Subject: [PATCH 21/68] chore(deps): bump whatwg-fetch from 2.0.4 to 3.4.0 Bumps [whatwg-fetch](https://github.com/github/fetch) from 2.0.4 to 3.4.0. - [Release notes](https://github.com/github/fetch/releases) - [Commits](https://github.com/github/fetch/compare/v2.0.4...v3.4.0) Signed-off-by: dependabot-preview[bot] --- plugins/catalog/package.json | 2 +- plugins/graphql/package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 2c9c09f688..0f82add554 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -54,7 +54,7 @@ "jest-fetch-mock": "^3.0.3", "msw": "^0.20.5", "react-test-renderer": "^16.13.1", - "whatwg-fetch": "^2.0.0" + "whatwg-fetch": "^3.4.0" }, "files": [ "dist" diff --git a/plugins/graphql/package.json b/plugins/graphql/package.json index 3aaabdd3f9..045cd471a1 100644 --- a/plugins/graphql/package.json +++ b/plugins/graphql/package.json @@ -27,7 +27,7 @@ "express": "^4.17.1", "express-promise-router": "^3.0.3", "graphql": "^15.3.0", - "whatwg-fetch": "^2.0.0", + "whatwg-fetch": "^3.4.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/yarn.lock b/yarn.lock index 0c7305b955..e039de0a47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22598,15 +22598,15 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" -whatwg-fetch@^2.0.0, whatwg-fetch@^2.0.4: +whatwg-fetch@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== -whatwg-fetch@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" - integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== +whatwg-fetch@^3.0.0, whatwg-fetch@^3.4.0: + version "3.4.0" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz#e11de14f4878f773fbebcde8871b2c0699af8b30" + integrity sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ== whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" From 2bb585f4fe9670c2f41881b48368cf0d9fc02b23 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Thu, 27 Aug 2020 13:15:09 +0200 Subject: [PATCH 22/68] TechDocs: add note to docs about limitations to the scaffolder (#2134) * add warning to docs about limitations to the scaffolder * Update docs/features/techdocs/creating-and-publishing.md --- docs/features/techdocs/creating-and-publishing.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index abb56b348a..037c4be172 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -30,6 +30,11 @@ the documentation template. Create an entity from the documentation template and you will get the needed setup for free. +!!! warning Currently the Backstage Software Templates are limited to create repositories +inside GitHub organizations. You also need to generate an personal access token +and use as an environment variable. Read more about this +[here](../software-templates/installation.md#runtime-dependencies). + ### Manually add documentation setup to already existing repository Prerequisities: From a8edb19f61e52254a591129af0a327e6db14b681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Thu, 27 Aug 2020 13:27:22 +0200 Subject: [PATCH 23/68] [Docs] Various fixes (#2136) * [Docs] Various fixes * More fixes * Fix ADRs * Typo --- docs/FAQ.md | 4 +- docs/README.md | 106 +-------------- docs/architecture-decisions/index.md | 7 +- .../getting-started/create-app_output.png | Bin .../getting-started/create-plugin_output.png | Bin .../software-catalog/bsc-edit.png | Bin .../software-catalog/bsc-extend.png | Bin .../software-catalog/bsc-register-1.png | Bin .../software-catalog/bsc-register-2.png | Bin .../software-catalog/bsc-search.png | Bin .../software-catalog/bsc-starred.png | Bin .../software-model-core-entities.png | Bin docs/auth/auth-backend-classes.md | 5 +- docs/auth/oauth.md | 4 +- .../software-catalog/extending-the-model.md | 9 +- docs/features/software-catalog/index.md | 23 ++-- .../features/software-catalog/system-model.md | 48 +++---- .../development-environment.md | 4 - docs/overview/background.md | 30 +++++ docs/plugins/create-a-plugin.md | 18 +-- docs/plugins/plugin-development.md | 22 +--- docs/reference/createPlugin-router.md | 2 - docs/tutorials/index.md | 6 - docs/{ => tutorials}/journey.md | 19 +-- microsite/i18n/en.json | 18 +-- microsite/pages/en/background.js | 122 ------------------ microsite/sidebars.json | 4 +- microsite/siteConfig.js | 4 - mkdocs.yml | 1 + 29 files changed, 118 insertions(+), 338 deletions(-) rename docs/{ => assets}/getting-started/create-app_output.png (100%) rename docs/{ => assets}/getting-started/create-plugin_output.png (100%) rename docs/{features => assets}/software-catalog/bsc-edit.png (100%) rename docs/{features => assets}/software-catalog/bsc-extend.png (100%) rename docs/{features => assets}/software-catalog/bsc-register-1.png (100%) rename docs/{features => assets}/software-catalog/bsc-register-2.png (100%) rename docs/{features => assets}/software-catalog/bsc-search.png (100%) rename docs/{features => assets}/software-catalog/bsc-starred.png (100%) rename docs/{features => assets}/software-catalog/software-model-core-entities.png (100%) create mode 100644 docs/overview/background.md delete mode 100644 docs/tutorials/index.md rename docs/{ => tutorials}/journey.md (94%) delete mode 100644 microsite/pages/en/background.js diff --git a/docs/FAQ.md b/docs/FAQ.md index d230e682a4..4b5df3ace0 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -3,7 +3,7 @@ id: FAQ title: FAQ --- -## Product FAQ: +## Product FAQ ### Can we call Backstage something different? So that it fits our company better? @@ -67,7 +67,7 @@ valuable as you grow. Yes! The Backstage UI is built using Material-UI. With the theming capabilities of Material-UI, you are able to adapt the interface to your brand guidelines. -## Technical FAQ: +## Technical FAQ ### Why Material-UI? diff --git a/docs/README.md b/docs/README.md index 55e178b691..c63a12b589 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,105 +1,3 @@ -# Documentation structure +# Documentation -**Note!** This documentation structure is very much work in progress. If (when, -really 😆) you find broken links or missing content, please create an issue or, -better yet, a pull request. - -# Plugins - -- 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) - - [Strategies for adopting](overview/adopting.md) -- Getting started - - [Running Backstage locally](getting-started/index.md) - - [Installation](getting-started/installation.md) - - [Local development](getting-started/development-environment.md) - - [Demo deployment](https://backstage-demo.roadie.io) - - Production deployments - - [Create an App](getting-started/create-an-app.md) - - App configuration - - [Configuring App with plugins](getting-started/configure-app-with-plugins.md) - - [Customize the look-and-feel of your App](getting-started/app-custom-theme.md) - - Deployment scenarios - - [Kubernetes](getting-started/deployment-k8s.md) - - [Other](getting-started/deployment-other.md) -- Features - - Software Catalog - - [Overview](features/software-catalog/index.md) - - [System model](features/software-catalog/system-model.md) - - [YAML File Format](features/software-catalog/descriptor-format.md) - - [Extending the model](features/software-catalog/extending-the-model.md) - - [External integrations](features/software-catalog/external-integrations.md) - - [API](features/software-catalog/api.md) - - Software creation templates - - [Overview](features/software-templates/index.md) - - [Adding templates](features/software-templates/adding-templates.md) - - Extending the Scaffolder: - - [Overview](features/software-templates/extending/index.md) - - [Create your own Templater](features/software-templates/extending/create-your-own-templater.md) - - [Create your own Publisher](features/software-templates/extending/create-your-own-publisher.md) - - [Create your own Preparer](features/software-templates/extending/create-your-own-preparer.md) - - Docs-like-code - - [Overview](features/techdocs/README.md) - - [Getting Started](features/techdocs/getting-started.md) - - [Concepts](features/techdocs/concepts.md) - - [Creating and Publishing Documentation](features/techdocs/creating-and-publishing.md) - - [FAQ](features/techdocs/FAQ.md) -- Plugins - - [Overview](plugins/index.md) - - [Existing plugins](plugins/existing-plugins.md) - - [Creating a new plugin](plugins/create-a-plugin.md) - - [Developing a plugin](plugins/plugin-development.md) - - [Structure of a plugin](plugins/structure-of-a-plugin.md) - - Backends and APIs - - [Proxying](plugins/proxying.md) - - [Backstage backend plugin](plugins/backend-plugin.md) - - [Call existing API](plugins/call-existing-api.md) - - Testing - - [Overview](plugins/testing.md) - - Publishing - - [Open source and NPM](plugins/publishing.md) - - [Private/internal (non-open source)](plugins/publish-private.md) -- Configuration - - [Overview](conf/index.md) - - [Reading Configuration](conf/reading.md) - - [Writing Configuration](conf/writing.md) - - [Defining Configuration](conf/defining.md) -- Authentication and identity - - [Overview](auth/index.md) - - [Add auth provider](auth/add-auth-provider.md) - - [Auth backend](auth/auth-backend.md) - - [OAuth](auth/oauth.md) - - [Glossary](auth/glossary.md) -- Designing for Backstage - - [Backstage Design Language System (DLS)](dls/design.md) - - [Storybook -- reusable UI components](http://backstage.io/storybook) - - [Contributing to Storybook](dls/contributing-to-storybook.md) - - [Figma resources](dls/figma.md) -- API references - - TypeScript API - - [Utility APIs](api/utility-apis.md) - - [Utility API References](reference/utility-apis/README.md) - - [createPlugin](reference/createPlugin.md) - - [createPlugin-feature-flags](reference/createPlugin-feature-flags.md) - - [createPlugin-router](reference/createPlugin-router.md) - - Backend APIs - - [Backend](api/backend.md) -- Tutorials - - [Overview](tutorials/index.md) -- Architecture Decision Records (ADRs) - - [Overview](architecture-decisions/index.md) - - [ADR001 - Architecture Decision Record (ADR) log](architecture-decisions/adr001-add-adr-log.md) - - [ADR002 - Default Software Catalog File Format](architecture-decisions/adr002-default-catalog-file-format.md) - - [ADR003 - Avoid Default Exports and Prefer Named Exports](architecture-decisions/adr003-avoid-default-exports.md) - - [ADR004 - Module Export Structure](architecture-decisions/adr004-module-export-structure.md) - - [ADR005 - Catalog Core Entities](architecture-decisions/adr005-catalog-core-entities.md) - - [ADR006 - Avoid React.FC and React.SFC](architecture-decisions/adr006-avoid-react-fc.md) - - [ADR007 - Use MSW for Mocking Network Requests](architecture-decisions/adr007-use-msw-to-mock-service-requests.md) - - [ADR008 - Default Catalog File Name](architecture-decisions/adr008-default-catalog-file-name.md) -- [Contribute](../CONTRIBUTING.md) -- [Support](overview/support.md) -- [FAQ](FAQ.md) +The Backstage documentation is available at https://backstage.io/docs diff --git a/docs/architecture-decisions/index.md b/docs/architecture-decisions/index.md index b02e8deabb..91d2c668d7 100644 --- a/docs/architecture-decisions/index.md +++ b/docs/architecture-decisions/index.md @@ -4,8 +4,6 @@ title: Architecture Decision Records (ADR) sidebar_label: Overview --- -# - The substantial architecture decisions made in the Backstage project lives here. For more information about ADRs, when to write them, and why, please see [this blog post](https://engineering.atspotify.com/2020/04/14/when-should-i-write-an-architecture-decision-record/). @@ -25,7 +23,10 @@ Records should be stored under the `architecture-decisions` directory. - Submit a pull request - Address and integrate feedback from the community - Eventually, assign a number -- Add the full path of the ADR to the [`mkdocs.yml`](/mkdocs.yml) +- Add the path of the ADR to the microsite sidebar in + [`sidebars.json`](https://github.com/spotify/backstage/blob/master/microsite/sidebars.json) +- Add the path of the ADR to the + [`mkdocs.yml`](https://github.com/spotify/backstage/blob/master/mkdocs.yml) - Merge the pull request ## Superseding an ADR diff --git a/docs/getting-started/create-app_output.png b/docs/assets/getting-started/create-app_output.png similarity index 100% rename from docs/getting-started/create-app_output.png rename to docs/assets/getting-started/create-app_output.png diff --git a/docs/getting-started/create-plugin_output.png b/docs/assets/getting-started/create-plugin_output.png similarity index 100% rename from docs/getting-started/create-plugin_output.png rename to docs/assets/getting-started/create-plugin_output.png diff --git a/docs/features/software-catalog/bsc-edit.png b/docs/assets/software-catalog/bsc-edit.png similarity index 100% rename from docs/features/software-catalog/bsc-edit.png rename to docs/assets/software-catalog/bsc-edit.png diff --git a/docs/features/software-catalog/bsc-extend.png b/docs/assets/software-catalog/bsc-extend.png similarity index 100% rename from docs/features/software-catalog/bsc-extend.png rename to docs/assets/software-catalog/bsc-extend.png diff --git a/docs/features/software-catalog/bsc-register-1.png b/docs/assets/software-catalog/bsc-register-1.png similarity index 100% rename from docs/features/software-catalog/bsc-register-1.png rename to docs/assets/software-catalog/bsc-register-1.png diff --git a/docs/features/software-catalog/bsc-register-2.png b/docs/assets/software-catalog/bsc-register-2.png similarity index 100% rename from docs/features/software-catalog/bsc-register-2.png rename to docs/assets/software-catalog/bsc-register-2.png diff --git a/docs/features/software-catalog/bsc-search.png b/docs/assets/software-catalog/bsc-search.png similarity index 100% rename from docs/features/software-catalog/bsc-search.png rename to docs/assets/software-catalog/bsc-search.png diff --git a/docs/features/software-catalog/bsc-starred.png b/docs/assets/software-catalog/bsc-starred.png similarity index 100% rename from docs/features/software-catalog/bsc-starred.png rename to docs/assets/software-catalog/bsc-starred.png diff --git a/docs/features/software-catalog/software-model-core-entities.png b/docs/assets/software-catalog/software-model-core-entities.png similarity index 100% rename from docs/features/software-catalog/software-model-core-entities.png rename to docs/assets/software-catalog/software-model-core-entities.png diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md index 42ee48c5e0..9034a3d054 100644 --- a/docs/auth/auth-backend-classes.md +++ b/docs/auth/auth-backend-classes.md @@ -1,4 +1,7 @@ -# Authentication Backend Classes Layout and Description +--- +id: auth-backend-classes +title: Auth backend classes +--- ## How Does Authentication Work? diff --git a/docs/auth/oauth.md b/docs/auth/oauth.md index 6e6dc4da80..b7013a310d 100644 --- a/docs/auth/oauth.md +++ b/docs/auth/oauth.md @@ -104,6 +104,8 @@ request an access token. The following diagram visualizes the flow described in the previous section. +![](oauth-popup-flow.svg) + - -![](oauth-popup-flow.svg) diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index ce5ea03b26..8b621f2e44 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -12,7 +12,7 @@ Backstage natively supports tracking of the following component - Documentation - Other -![](bsc-extend.png) +![](../../assets/software-catalog/bsc-extend.png) Since these types are likely not the only kind of software you will want to track in Backstage, it is possible to @@ -31,9 +31,10 @@ catalog. It might be tempting to put software that doesn't fit into any of the existing types into Other. There are a few reasons why we advice against this; firstly, we have found that it is preferred to match the conceptual model that your -engineers have when describing your sofware. Secondly, Backstage helps your -engineers manage their software by integrating the infratrucure tooling through -plugins. Different plugins are used for managing different types of components. +engineers have when describing your software. Secondly, Backstage helps your +engineers manage their software by integrating the infrastructure tooling +through plugins. Different plugins are used for managing different types of +components. For example, the [Lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse) diff --git a/docs/features/software-catalog/index.md b/docs/features/software-catalog/index.md index fe107d1f59..db8c183390 100644 --- a/docs/features/software-catalog/index.md +++ b/docs/features/software-catalog/index.md @@ -1,6 +1,7 @@ --- id: software-catalog-overview title: Backstage Service Catalog (alpha) +sidebar_label: Backstage Service Catalog --- ## What is a Service Catalog? @@ -54,18 +55,18 @@ There are 3 ways to add components to the catalog: Users can register new components by going to `/create` and clicking the **REGISTER EXISTING COMPONENT** button: -![](bsc-register-1.png) +![](../../assets/software-catalog/bsc-register-1.png) Backstage expects the full URL to the YAML in your source control. Example: -``` +```bash https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml ``` _More examples can be found [here](https://github.com/spotify/backstage/tree/master/packages/catalog-model/examples)._ -![](bsc-register-2.png) +![](../../assets/software-catalog/bsc-register-2.png) It is important to note that any kind of software can be registered in Backstage. Even if the software is not maintained by your company (SaaS @@ -83,7 +84,7 @@ registered in the catalog. Teams owning the components are responsible for maintaining the metadata about them, and do so using their normal Git workflow. -![](bsc-edit.png) +![](../../assets/software-catalog/bsc-edit.png) Once the change has been merged, Backstage will automatically show the updated metadata in the service catalog after a short while. @@ -92,25 +93,25 @@ metadata in the service catalog after a short while. By default the service catalog shows components owned by the team of the logged in user. But you can also switch to _All_ to see all the components across your -companie's software ecosystem. Basic inline _search_ and _column filtering_ -makes it easy to browse a big set of components. +company's software ecosystem. Basic inline _search_ and _column filtering_ makes +it easy to browse a big set of components. -![](bsc-search.png) +![](../../assets/software-catalog/bsc-search.png) ## Starring components For easy and quick access to components you visit frequently, Backstage supports _starring_ of components: -![](bsc-starred.png) +![](../../assets/software-catalog/bsc-starred.png) ## Integrated tooling through plugins -The service catalog is a great way to organise the infrastructure tools you use +The service catalog is a great way to organize the infrastructure tools you use to manage the software. This is how Backstage creates one developer portal for all your tools. Rather than asking teams to jump between different -infrastructure UI’s (and incurring additional cognitive overhead each time they -make a context switch), most of these tools can be organised around the entities +infrastructure UIs (and incurring additional cognitive overhead each time they +make a context switch), most of these tools can be organized around the entities in the catalog. ![tools](https://backstage.io/blog/assets/20-05-20/tabs.png) diff --git a/docs/features/software-catalog/system-model.md b/docs/features/software-catalog/system-model.md index 435fe05b48..4436f60fc2 100644 --- a/docs/features/software-catalog/system-model.md +++ b/docs/features/software-catalog/system-model.md @@ -16,11 +16,13 @@ We model software in the Backstage catalogue using these three core entities (further explained below): - **Components** are individual pieces of software + - **APIs** are the boundaries between different components + - **Resources** are physical or virtual infrastructure needed to operate a component -![](system-model-core-entities.png) +![](../../assets/software-catalog/software-model-core-entities.png) ### Component @@ -29,22 +31,22 @@ backend service or data pipeline (list not exhaustive). A component can be tracked in source control, or use some existing open source or commercial software. -A component can implement APIs for other components to consume. In turn it -might depend on APIs implemented by other components, or resources that are -attached to it at runtime. +A component can implement APIs for other components to consume. In turn it might +depend on APIs implemented by other components, or resources that are attached +to it at runtime. ### API APIs form an important (maybe the most important) abstraction that allows large software ecosystems to scale. Thus, APIs are a first class citizen in the -Backstage model and the primary way to discover existing functionality in -the ecosystem. +Backstage model and the primary way to discover existing functionality in the +ecosystem. APIs are implemented by components and form boundaries between components. They -might be defined using an RPC IDL (eg Protobuf, GraphQL, ...), a data schema -(eg Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by -components need to be in a known machine-readable format so we can -build further tooling and analysis on top. +might be defined using an RPC IDL (eg Protobuf, GraphQL, ...), a data schema (eg +Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by +components need to be in a known machine-readable format so we can build further +tooling and analysis on top. APIs have a visibility: they are either public (making them available for any other component to consume), restricted (only available to a whitelisted set of @@ -62,12 +64,13 @@ footprint, and create tooling around them. ## Ecosystem Modeling -A large catalogue of components, APIs and resources can be highly granular -and hard to understand as a whole. It might thus be convenient to further -categorize these entities using the following (optional) concepts: -* **Systems** are a collection of entities that cooperate to perform some - function -* **Domains** relate entities and systems to part of the business +A large catalogue of components, APIs and resources can be highly granular and +hard to understand as a whole. It might thus be convenient to further categorize +these entities using the following (optional) concepts: + +- **Systems** are a collection of entities that cooperate to perform some + function +- **Domains** relate entities and systems to part of the business ### System @@ -82,15 +85,16 @@ exposes one or several public APIs. The main benefit of modelling a system is that it hides its resources and private APIs between the components for any consumers. This means that as the owner, you can evolve the implementation, in terms of components and resources, without your consumers being able to notice. -Typically, a system will consist of at most a handful of components (see -Domain for a grouping of systems). +Typically, a system will consist of at most a handful of components (see Domain +for a grouping of systems). -For example, a playlist management system might encapsulate a backend service -to update playlists, a backend service to query them, and a database to store -them. It could expose an RPC API, a daily snapshots dataset, and an event -stream of playlist updates. +For example, a playlist management system might encapsulate a backend service to +update playlists, a backend service to query them, and a database to store them. +It could expose an RPC API, a daily snapshots dataset, and an event stream of +playlist updates. ### Domain + While systems are the basic level of encapsulation for related entities, it is often useful to group a collection of systems that share terminology, domain models, metrics, KPIs, business purpose, or documentation, i.e. they form a diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index 6a3a2cc7a2..421ce6d2c5 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -85,7 +85,3 @@ yarn create-plugin # Create a new plugin > See > [package.json](https://github.com/spotify/backstage/blob/master/package.json) > for other yarn commands/options. - -[Next Step - Create a Backstage plugin](../plugins/create-a-plugin.md) - -[Back to Docs](../README.md) diff --git a/docs/overview/background.md b/docs/overview/background.md new file mode 100644 index 0000000000..ef226653fb --- /dev/null +++ b/docs/overview/background.md @@ -0,0 +1,30 @@ +--- +id: background +title: The Spotify Story +--- + +Backstage was born out of necessity at Spotify. We found that as we grew, our +infrastructure was becoming more fragmented, our engineers less productive. + +Instead of building and testing code, teams were spending more time looking for +the right information just to get started. “Where’s the API for that service +we’re all supposed to be using?” “What version of that framework is everyone +on?” “This service isn’t responding, who owns it?” “I can’t find documentation +for anything!” + +Context switching and cognitive overload were dragging engineers down, day by +day. We needed to make it easier for our engineers to do their work without +having to become an expert in every aspect of infrastructure tooling. + +Our idea was to centralize and simplify end-to-end software development with an +abstraction layer that sits on top of all of our infrastructure and developer +tooling. That’s Backstage. + +It’s a developer portal powered by a centralized service catalog — with a plugin +architecture that makes it endlessly extensible and customizable. + +Manage all your services, software, tooling, and testing in Backstage. Start +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. diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index 0ca83b076c..4bcc4da387 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -15,20 +15,16 @@ dependencies, then run the following on your command line (invoking the yarn create-plugin ``` -

- create plugin -

+![](../assets/getting-started/create-plugin_output.png) This will create a new Backstage Plugin based on the ID that was provided. It will be built and added to the Backstage App automatically. -_If `yarn start` is already running you should be able to see the default page -for your new plugin directly by navigating to -`http://localhost:3000/my-plugin`._ +> If `yarn start` is already running you should be able to see the default page +> for your new plugin directly by navigating to +> `http://localhost:3000/my-plugin`. -

- my plugin -

+![](../assets/my-plugin_screenshot.png) You can also serve the plugin in isolation by running `yarn start` in the plugin directory. Or by using the yarn workspace command, for example: @@ -40,7 +36,3 @@ yarn workspace @backstage/plugin-welcome start # Also supports --check 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 plugin's `dev/` directory. - -[Next Step - Structure of a plugin](structure-of-a-plugin.md) - -[Back to Getting Started](../README.md) diff --git a/docs/plugins/plugin-development.md b/docs/plugins/plugin-development.md index b5c3240de0..faf3c6ddbd 100644 --- a/docs/plugins/plugin-development.md +++ b/docs/plugins/plugin-development.md @@ -1,6 +1,6 @@ --- id: plugin-development -title: Plugin Development in Backstage +title: Plugin Development --- Backstage plugins provide features to a Backstage App. @@ -10,28 +10,12 @@ type of content. Plugins all use a common set of platform APIs and reusable UI components. Plugins can fetch data from external sources using the regular browser APIs or by depending on external modules to do the work. - - ## Developing guidelines - Consider writing plugins in `TypeScript`. - Plan the directory structure of your plugin so that it becomes easy to manage. -- Prefer using the Backstage components, otherwise go with - [Material-UI](https://material-ui.com/). +- Prefer using the [Backstage components](https://backstage.io/storybook), + otherwise go with [Material-UI](https://material-ui.com/). - Check out the shared Backstage APIs before building a new one. ## Plugin concepts / API diff --git a/docs/reference/createPlugin-router.md b/docs/reference/createPlugin-router.md index ebefa13bb0..361b485df9 100644 --- a/docs/reference/createPlugin-router.md +++ b/docs/reference/createPlugin-router.md @@ -37,5 +37,3 @@ const myPluginRouteRef = createRouteRef({ title: 'My Plugin', }); ``` - -[Back to References](../README.md) diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md deleted file mode 100644 index b5780875b2..0000000000 --- a/docs/tutorials/index.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -id: index -title: Overview ---- - -## Coming soon! diff --git a/docs/journey.md b/docs/tutorials/journey.md similarity index 94% rename from docs/journey.md rename to docs/tutorials/journey.md index e38130afb7..3c399c53ba 100644 --- a/docs/journey.md +++ b/docs/tutorials/journey.md @@ -1,12 +1,15 @@ -# Purpose +--- +id: journey +title: Future developer journey +--- -This RFC describes a possible journey of a future Backstage plugin developer as -they build a plugin that touches many different aspects of a Backstage. The -story invents many new things that are not part of Backstage today, but are -things that I'm suggesting we should add as long term or north star goals. The -idea is to discuss what parts of the story makes sense to aim for, and what we'd -want to do differently or not at all. The "chapters" are numbered to make it a -bit easier to comment on parts of the story. +> This document describes a possible journey of a **_future_** Backstage plugin +> developer as they build a plugin that touches many different aspects of a +> Backstage. The story invents many new things that are not part of Backstage +> today, but are things that I'm suggesting we should add as long term or north +> star goals. The idea is to discuss what parts of the story makes sense to aim +> for, and what we'd want to do differently or not at all. The "chapters" are +> numbered to make it a bit easier to comment on parts of the story. # The Protagonist diff --git a/microsite/i18n/en.json b/microsite/i18n/en.json index 96c874f496..9d83dfcef9 100644 --- a/microsite/i18n/en.json +++ b/microsite/i18n/en.json @@ -51,7 +51,7 @@ "title": "Adding authentication providers" }, "auth/auth-backend-classes": { - "title": "auth/auth-backend-classes" + "title": "Auth backend classes" }, "auth/auth-backend": { "title": "Auth backend" @@ -103,7 +103,8 @@ "title": "External integrations" }, "features/software-catalog/software-catalog-overview": { - "title": "Backstage Service Catalog (alpha)" + "title": "Backstage Service Catalog (alpha)", + "sidebar_label": "Backstage Service Catalog" }, "features/software-catalog/installation": { "title": "features/software-catalog/installation" @@ -174,9 +175,6 @@ "getting-started/installation": { "title": "Installation" }, - "journey": { - "title": "journey" - }, "overview/adopting": { "title": "Strategies for adopting" }, @@ -186,6 +184,9 @@ "overview/architecture-terminology": { "title": "Architecture terminology" }, + "overview/background": { + "title": "The Spotify Story" + }, "overview/roadmap": { "title": "Project roadmap" }, @@ -214,7 +215,7 @@ "title": "Intro to plugins" }, "plugins/plugin-development": { - "title": "Plugin Development in Backstage" + "title": "Plugin Development" }, "plugins/proxying": { "title": "Proxying" @@ -285,8 +286,8 @@ "reference/utility-apis/StorageApi": { "title": "reference/utility-apis/StorageApi" }, - "tutorials/index": { - "title": "Overview" + "tutorials/journey": { + "title": "Future developer journey" } }, "links": { @@ -294,7 +295,6 @@ "Docs": "Docs", "Blog": "Blog", "Demos": "Demos", - "The Spotify story": "The Spotify story", "Newsletter": "Newsletter" }, "categories": { diff --git a/microsite/pages/en/background.js b/microsite/pages/en/background.js deleted file mode 100644 index c5bf9bee94..0000000000 --- a/microsite/pages/en/background.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const React = require('react'); -const Components = require(`${process.cwd()}/core/Components.js`); -const Block = Components.Block; -const Breakpoint = Components.Breakpoint; - -const Background = props => { - const { config: siteConfig } = props; - const { baseUrl } = siteConfig; - return ( -
- - - The Spotify Story - - - Backstage was born out of necessity at Spotify. We found that as - we grew, our infrastructure was becoming more fragmented, our - engineers less productive.{' '} - - - - Instead of building and testing code, teams were spending more - time looking for the right information just to get started. - “Where’s the API for that service we’re all supposed to be using?” - “What version of that framework is everyone on?” “This service - isn’t responding, who owns it?” “I can’t find documentation for - anything!”{' '} - - - - - - - One place for everything. Accessible to everyone. - - - - } - > - - Context switching and cognitive overload were dragging engineers - down, day by day. We needed to make it easier for our engineers to - do their work without having to become an expert in every aspect - of infrastructure tooling. - - - - Our idea was to centralize and simplify end-to-end software - development with an abstraction layer that sits on top of all of - our infrastructure and developer tooling. That’s Backstage. - - - - It’s a developer portal powered by a centralized service catalog — - with a plugin architecture that makes it endlessly extensible and - customizable. - - - - Manage all your services, software, tooling, and testing in - Backstage. Start 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. - - - - Explore Features - - - } - > - - - - - - - - - One place for everything. Accessible to everyone. - - - Explore Features - - - - } - > - -
-
- ); -}; - -module.exports = Background; diff --git a/microsite/sidebars.json b/microsite/sidebars.json index efb76397aa..72a9388818 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -6,6 +6,7 @@ "overview/architecture-terminology", "overview/roadmap", "overview/vision", + "overview/background", "overview/adopting" ], "Getting Started": [ @@ -38,7 +39,6 @@ "features/software-catalog/software-catalog-overview", "features/software-catalog/system-model", "features/software-catalog/descriptor-format", - "features/software-catalog/populating-catalog", "features/software-catalog/extending-the-model", "features/software-catalog/external-integrations", "features/software-catalog/software-catalog-api" @@ -132,7 +132,7 @@ "ids": ["api/backend"] } ], - "Tutorials": ["tutorials/index"], + "Tutorials": ["tutorials/journey"], "Architecture Decision Records (ADRs)": [ "architecture-decisions/adrs-overview", "architecture-decisions/adrs-adr001", diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js index 631dc4c5fa..667572a58e 100644 --- a/microsite/siteConfig.js +++ b/microsite/siteConfig.js @@ -47,10 +47,6 @@ const siteConfig = { page: 'demos', label: 'Demos', }, - { - page: 'background', - label: 'The Spotify story', - }, { href: 'https://mailchi.mp/spotify/backstage-community', label: 'Newsletter', diff --git a/mkdocs.yml b/mkdocs.yml index 74f9815483..637e45c0f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,7 @@ nav: - Architecture and terminology: 'overview/architecture-terminology.md' - Roadmap: 'overview/roadmap.md' - Vision: 'overview/vision.md' + - The Spotify story: 'overview/background.md' - Strategies for adopting: 'overview/adopting.md' - Getting started: - Running Backstage locally: 'getting-started/index.md' From eb613c1aac018cf3cd526300aec9f1e1a5a475ac Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2020 13:27:55 +0200 Subject: [PATCH 24/68] chore(deps): bump apollo-server from 2.16.0 to 2.16.1 (#2133) Bumps [apollo-server](https://github.com/apollographql/apollo-server/tree/HEAD/packages/apollo-server) from 2.16.0 to 2.16.1. - [Release notes](https://github.com/apollographql/apollo-server/releases) - [Changelog](https://github.com/apollographql/apollo-server/blob/main/CHANGELOG.md) - [Commits](https://github.com/apollographql/apollo-server/commits/apollo-server@2.16.1/packages/apollo-server) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0c7305b955..d56a2cbd2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5573,7 +5573,7 @@ apollo-server-caching@^0.5.2: dependencies: lru-cache "^5.0.0" -apollo-server-core@^2.16.0, apollo-server-core@^2.16.1: +apollo-server-core@^2.16.1: version "2.16.1" resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.16.1.tgz#5b5b8245ab9c0cb6c2367ec19ab855dea6ccea3a" integrity sha512-nuwn5ZBbmzPwDetb3FgiFFJlNK7ZBFg8kis/raymrjd3eBGdNcOyMTJDl6J9673X9Xqp+dXQmFYDW/G3G8S1YA== @@ -5614,7 +5614,7 @@ apollo-server-errors@^2.4.2: resolved "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.4.2.tgz#1128738a1d14da989f58420896d70524784eabe5" integrity sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ== -apollo-server-express@^2.16.0: +apollo-server-express@^2.16.0, apollo-server-express@^2.16.1: version "2.16.1" resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.16.1.tgz#7438bca590ef8577d24d20ba0b6d582c120c0146" integrity sha512-Oq5YNcaMYnRk6jDmA9LWf8oSd2KHDVe7jQ4wtooAvG9FVUD+FaFBgSkytXHMvtifQh2wdF07Ri8uDLMz6IQjTw== @@ -5653,12 +5653,12 @@ apollo-server-types@^0.5.1: apollo-server-env "^2.4.5" apollo-server@^2.16.0: - version "2.16.0" - resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-2.16.0.tgz#fa30e29b78e8cb70b2c81d0f7b96953beb3d4baf" - integrity sha512-zbEe0FSqatqE6bmIfq/o1/Hsqc596ZOwZM/L8Ttwa4ucQ1ybqf1ZejSYu6ehFEj1G6rOBY1ttVKkIllcErK4GQ== + version "2.16.1" + resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-2.16.1.tgz#edc319606eb29f73132239bdc005dd88ac40a142" + integrity sha512-oy9NVRzGwlpQ+W1DwLKRH+KASmodSYpvYIRY5DMAZtGqNmT2zOCpbIZVjBt23SuPB5NhIhhE4ROzoObRv3zy5w== dependencies: - apollo-server-core "^2.16.0" - apollo-server-express "^2.16.0" + apollo-server-core "^2.16.1" + apollo-server-express "^2.16.1" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" From 3fbfb4261948a4c6101eff8b6e26681980c74186 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Thu, 27 Aug 2020 14:01:09 +0200 Subject: [PATCH 25/68] register techdocs storage api to dev app (#2139) --- plugins/techdocs/dev/api.ts | 52 ++++++++++++++++++++++++++++++++++ plugins/techdocs/dev/index.tsx | 14 ++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 plugins/techdocs/dev/api.ts diff --git a/plugins/techdocs/dev/api.ts b/plugins/techdocs/dev/api.ts new file mode 100644 index 0000000000..60a17c3dc3 --- /dev/null +++ b/plugins/techdocs/dev/api.ts @@ -0,0 +1,52 @@ +/* + * 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 { ParsedEntityId } from '../src/types'; + +import { TechDocsStorage } from '../src/api'; + +export class TechDocsDevStorageApi implements TechDocsStorage { + public apiOrigin: string; + + constructor({ apiOrigin }: { apiOrigin: string }) { + this.apiOrigin = apiOrigin; + } + + async getEntityDocs(entityId: ParsedEntityId, path: string) { + const { name } = entityId; + + const url = `${this.apiOrigin}/${name}/${path}`; + + const request = await fetch( + `${url.endsWith('/') ? url : `${url}/`}index.html`, + ); + + if (request.status === 404) { + throw new Error('Page not found'); + } + + return request.text(); + } + + getBaseUrl( + oldBaseUrl: string, + entityId: ParsedEntityId, + path: string, + ): string { + const { name } = entityId; + return new URL(oldBaseUrl, `${this.apiOrigin}/${name}/${path}`).toString(); + } +} diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index 812a5585d4..b415256fde 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -16,5 +16,17 @@ import { createDevApp } from '@backstage/dev-utils'; import { plugin } from '../src/plugin'; +import { TechDocsDevStorageApi } from './api'; +import { techdocsStorageApiRef } from '../src'; -createDevApp().registerPlugin(plugin).render(); +createDevApp() + .registerApiFactory({ + deps: {}, + factory: () => + new TechDocsDevStorageApi({ + apiOrigin: 'http://localhost:3000/api', + }), + implements: techdocsStorageApiRef, + }) + .registerPlugin(plugin) + .render(); From e64411a754b34e267853f743de0ac6a97589b8dd Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 27 Aug 2020 15:10:39 +0200 Subject: [PATCH 26/68] fix(api-docs): fix a small styling issue in openapi rendering (#2141) Specs without operations were no correctly displayed in dark mode. --- .../OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx index 6edf39319e..d4fe2ab9e1 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx @@ -37,7 +37,7 @@ const useStyles = makeStyles(theme => ({ '& section.models, section.models.is-open h4': { 'border-color': theme.palette.divider, }, - '& .opblock .opblock-summary-description, .parameter__type, table.headers td, .model-title, .model .property.primitive': { + '& .opblock .opblock-summary-description, .parameter__type, table.headers td, .model-title, .model .property.primitive, section h3': { color: theme.palette.text.secondary, }, '& .opblock .opblock-summary-operation-id, .opblock .opblock-summary-path, .opblock .opblock-summary-path__deprecated, .opblock .opblock-section-header h4, .parameter__name, .response-col_status, .response-col_links, .responses-inner h4, .swagger-ui .responses-inner h5, .opblock-section-header .btn, .tab li, .info li, .info p, .info table, section.models h4, .info .title, table.model tr.description, .property-row': { From 9e084265fe856541150d3f12324a93f7d0096ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Thu, 27 Aug 2020 15:35:48 +0200 Subject: [PATCH 27/68] Enable search of docs on microsite (#2142) * Enable search of docs on microsite * Add proper config --- microsite/siteConfig.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js index 667572a58e..d6a2da9f91 100644 --- a/microsite/siteConfig.js +++ b/microsite/siteConfig.js @@ -110,6 +110,12 @@ const siteConfig = { stylesheets: [ 'https://fonts.googleapis.com/css?family=IBM+Plex+Mono:500,700&display=swap', ], + + algolia: { + apiKey: '8d115c9875ba0f4feaee95bab55a1645', + indexName: 'backstage', + searchParameters: {}, // Optional (if provided by Algolia) + }, }; module.exports = siteConfig; From 9a2e3b93de060e5ffb11c7a6b5550502a374ab6b Mon Sep 17 00:00:00 2001 From: ebarrios Date: Thu, 27 Aug 2020 17:39:22 +0200 Subject: [PATCH 28/68] Added Widget for listing workflows run --- .../src/components/Widget/Widget.tsx | 53 ++++++++++++++++++- .../src/components/Widget/index.ts | 2 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/plugins/github-actions/src/components/Widget/Widget.tsx b/plugins/github-actions/src/components/Widget/Widget.tsx index 031364c8ce..201004f46e 100644 --- a/plugins/github-actions/src/components/Widget/Widget.tsx +++ b/plugins/github-actions/src/components/Widget/Widget.tsx @@ -15,7 +15,7 @@ */ import React, { useEffect } from 'react'; import { useWorkflowRuns } from '../useWorkflowRuns'; -import { WorkflowRun } from '../WorkflowRunsTable'; +import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable'; import { Entity } from '@backstage/catalog-model'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { @@ -108,3 +108,54 @@ export const Widget = ({ ); }; + +const WidgetListContent = ({ + error, + loading, + branch, +}: { + error?: Error; + loading?: boolean; + lastRun: WorkflowRun; + branch: string; +}) => { + if (error) return Couldn't fetch {branch} runs; + if (loading) return ; + return ; +}; + +export const WidgetList = ({ + entity, + branch = 'master', +}: { + entity: Entity; + branch: string; +}) => { + const errorApi = useApi(errorApiRef); + const [owner, repo] = ( + entity?.metadata.annotations?.['backstage.io/github-actions-id'] ?? '/' + ).split('/'); + const [{ runs, loading, error }] = useWorkflowRuns({ + owner, + repo, + branch, + }); + + const lastRun = runs?.[0] ?? ({} as WorkflowRun); + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + + return ( + + + + ); +}; diff --git a/plugins/github-actions/src/components/Widget/index.ts b/plugins/github-actions/src/components/Widget/index.ts index 2b34671ab5..0efc84d952 100644 --- a/plugins/github-actions/src/components/Widget/index.ts +++ b/plugins/github-actions/src/components/Widget/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Widget } from './Widget'; +export { Widget, WidgetList } from './Widget'; From 96d67a24ce04a4714409f2772d7422ba471ebe08 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Thu, 27 Aug 2020 18:25:39 +0200 Subject: [PATCH 29/68] Added Widget for listing workflows run to catalog under CI/CD tab --- .../components/CatalogPage/CatalogLayout.tsx | 1 - .../components/CatalogPage/utils/timeUtil.js | 2 +- .../src/components/EntityPage/EntityPage.tsx | 2 ++ .../components/EntityPageCi/EntityPageCi.tsx | 35 +++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx diff --git a/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx b/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx index 39b3c66013..dfa4f2d39d 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx @@ -38,7 +38,6 @@ const CatalogLayout = ({ children }: Props) => {
diff --git a/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js b/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js index 327f4177a8..c32bebc4f8 100644 --- a/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js +++ b/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js @@ -67,6 +67,6 @@ export function getTimeBasedGreeting() { const greetingsKey = random(Object.keys(greetings)); return { language: greetingsKey, - greeting: greetings[greetingsKey], + greeting: greetings.English, }; } diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx index adbc758067..558f4b6551 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx @@ -36,6 +36,7 @@ import { catalogApiRef } from '../..'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { EntityPageDocs } from '../EntityPageDocs/EntityDocsPage'; import { EntityPageApi } from '../EntityPageApi/EntityPageApi'; +import { EntityPageCi } from '../EntityPageCi/EntityPageCi'; import { EntityPageOverview } from '../EntityPageOverview/EntityPageOverview'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; @@ -129,6 +130,7 @@ export const EntityPage: FC<{}> = () => { { id: 'ci', label: 'CI/CD', + content: (e: Entity) => , }, { id: 'tests', diff --git a/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx new file mode 100644 index 0000000000..01de6dc9a2 --- /dev/null +++ b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { Content } from '@backstage/core'; +import { WidgetList as GithubActionsListWidget } from '@backstage/plugin-github-actions'; +import { Grid } from '@material-ui/core'; +import React, { FC } from 'react'; + +export const EntityPageCi: FC<{ entity: Entity }> = ({ entity }) => { + return ( + + + {entity.metadata?.annotations?.['backstage.io/github-actions-id'] && ( + + + + )} + + + ); +}; From ffa9886051b3bb1aed694531898c8f9227da7ccd Mon Sep 17 00:00:00 2001 From: ebarrios Date: Thu, 27 Aug 2020 18:30:59 +0200 Subject: [PATCH 30/68] Added Widget for listing workflows run to catalog under CI/CD tab --- .../src/components/EntityPage/EntityPage.tsx | 2 ++ .../components/EntityPageCi/EntityPageCi.tsx | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx index adbc758067..558f4b6551 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx @@ -36,6 +36,7 @@ import { catalogApiRef } from '../..'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { EntityPageDocs } from '../EntityPageDocs/EntityDocsPage'; import { EntityPageApi } from '../EntityPageApi/EntityPageApi'; +import { EntityPageCi } from '../EntityPageCi/EntityPageCi'; import { EntityPageOverview } from '../EntityPageOverview/EntityPageOverview'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; @@ -129,6 +130,7 @@ export const EntityPage: FC<{}> = () => { { id: 'ci', label: 'CI/CD', + content: (e: Entity) => , }, { id: 'tests', diff --git a/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx new file mode 100644 index 0000000000..01de6dc9a2 --- /dev/null +++ b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { Content } from '@backstage/core'; +import { WidgetList as GithubActionsListWidget } from '@backstage/plugin-github-actions'; +import { Grid } from '@material-ui/core'; +import React, { FC } from 'react'; + +export const EntityPageCi: FC<{ entity: Entity }> = ({ entity }) => { + return ( + + + {entity.metadata?.annotations?.['backstage.io/github-actions-id'] && ( + + + + )} + + + ); +}; From 827eea8a5efadeb21480157fb779b4039db5b158 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Fri, 28 Aug 2020 07:47:25 +0100 Subject: [PATCH 31/68] Remove unused config values --- app-config.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 6202d48560..38f16e1ac3 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -136,8 +136,6 @@ auth: env: AUTH_AUTH0_DOMAIN microsoft: development: - appOrigin: 'http://localhost:3000/' - secure: false clientId: $secret: env: AUTH_MICROSOFT_CLIENT_ID From 7bbbfa1e6a958d6a083bf1d5021e4a2cb2ec88ea Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Fri, 28 Aug 2020 10:31:28 +0200 Subject: [PATCH 32/68] fix: discovery api leftovers --- packages/app/src/apis.ts | 3 +-- .../auth/microsoft/MicrosoftAuth.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index c23f09a5be..eedd07ed08 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -135,8 +135,7 @@ export const apis = (config: ConfigApi) => { builder.add( microsoftAuthApiRef, MicrosoftAuth.create({ - backendUrl, - basePath: '/auth/', + discoveryApi, oauthRequestApi, }), ); diff --git a/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index d4a70995e3..5800308c26 100644 --- a/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -30,15 +30,17 @@ import { BackstageIdentity, } from '../../../definitions/auth'; -import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { + OAuthRequestApi, + AuthProvider, + DiscoveryApi, +} from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { Observable } from '../../../../types'; type CreateOptions = { - backendUrl: string; - basePath: string; - + discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; environment?: string; @@ -70,15 +72,13 @@ class MicrosoftAuth BackstageIdentityApi, SessionStateApi { static create({ - backendUrl, - basePath, environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, + discoveryApi, }: CreateOptions) { const connector = new DefaultAuthConnector({ - backendUrl, - basePath, + discoveryApi, environment, provider, oauthRequestApi: oauthRequestApi, From 691b8ad8fbe44e465f9645e5d6fa0d08289b8a03 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 28 Aug 2020 10:56:05 +0200 Subject: [PATCH 33/68] Reverted change applied to master by mistake --- plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx | 1 + plugins/catalog/src/components/CatalogPage/utils/timeUtil.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx b/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx index dfa4f2d39d..39b3c66013 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx @@ -38,6 +38,7 @@ const CatalogLayout = ({ children }: Props) => {
diff --git a/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js b/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js index c32bebc4f8..327f4177a8 100644 --- a/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js +++ b/plugins/catalog/src/components/CatalogPage/utils/timeUtil.js @@ -67,6 +67,6 @@ export function getTimeBasedGreeting() { const greetingsKey = random(Object.keys(greetings)); return { language: greetingsKey, - greeting: greetings.English, + greeting: greetings[greetingsKey], }; } From b0a6218c1a65e07ea3a10a2623c1840ea6c5002a Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 28 Aug 2020 10:56:48 +0200 Subject: [PATCH 34/68] Reverted change applied to master by mistake --- plugins/catalog/src/components/EntityPage/EntityPage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx index 558f4b6551..adbc758067 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx @@ -36,7 +36,6 @@ import { catalogApiRef } from '../..'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { EntityPageDocs } from '../EntityPageDocs/EntityDocsPage'; import { EntityPageApi } from '../EntityPageApi/EntityPageApi'; -import { EntityPageCi } from '../EntityPageCi/EntityPageCi'; import { EntityPageOverview } from '../EntityPageOverview/EntityPageOverview'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; @@ -130,7 +129,6 @@ export const EntityPage: FC<{}> = () => { { id: 'ci', label: 'CI/CD', - content: (e: Entity) => , }, { id: 'tests', From 93b54256608ce7115c5f01c63eb9447ff71819f8 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 28 Aug 2020 10:57:11 +0200 Subject: [PATCH 35/68] Reverted change applied to master by mistake --- .../components/EntityPageCi/EntityPageCi.tsx | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx diff --git a/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx deleted file mode 100644 index 01de6dc9a2..0000000000 --- a/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Entity } from '@backstage/catalog-model'; -import { Content } from '@backstage/core'; -import { WidgetList as GithubActionsListWidget } from '@backstage/plugin-github-actions'; -import { Grid } from '@material-ui/core'; -import React, { FC } from 'react'; - -export const EntityPageCi: FC<{ entity: Entity }> = ({ entity }) => { - return ( - - - {entity.metadata?.annotations?.['backstage.io/github-actions-id'] && ( - - - - )} - - - ); -}; From de7c7268b3746e5f6abff90d5e84e1022ac0db85 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Thu, 27 Aug 2020 18:30:59 +0200 Subject: [PATCH 36/68] Added Widget for listing workflows run to catalog under CI/CD tab --- .../src/components/EntityPage/EntityPage.tsx | 2 ++ .../components/EntityPageCi/EntityPageCi.tsx | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx index adbc758067..558f4b6551 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx @@ -36,6 +36,7 @@ import { catalogApiRef } from '../..'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { EntityPageDocs } from '../EntityPageDocs/EntityDocsPage'; import { EntityPageApi } from '../EntityPageApi/EntityPageApi'; +import { EntityPageCi } from '../EntityPageCi/EntityPageCi'; import { EntityPageOverview } from '../EntityPageOverview/EntityPageOverview'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; @@ -129,6 +130,7 @@ export const EntityPage: FC<{}> = () => { { id: 'ci', label: 'CI/CD', + content: (e: Entity) => , }, { id: 'tests', diff --git a/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx new file mode 100644 index 0000000000..01de6dc9a2 --- /dev/null +++ b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { Content } from '@backstage/core'; +import { WidgetList as GithubActionsListWidget } from '@backstage/plugin-github-actions'; +import { Grid } from '@material-ui/core'; +import React, { FC } from 'react'; + +export const EntityPageCi: FC<{ entity: Entity }> = ({ entity }) => { + return ( + + + {entity.metadata?.annotations?.['backstage.io/github-actions-id'] && ( + + + + )} + + + ); +}; From babfe5716b7940d7f5737fb8dca2b7ead9fc157b Mon Sep 17 00:00:00 2001 From: ebarrios Date: Thu, 27 Aug 2020 17:39:22 +0200 Subject: [PATCH 37/68] Added Widget for listing workflows run --- .../src/components/Widget/Widget.tsx | 53 ++++++++++++++++++- .../src/components/Widget/index.ts | 2 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/plugins/github-actions/src/components/Widget/Widget.tsx b/plugins/github-actions/src/components/Widget/Widget.tsx index 031364c8ce..201004f46e 100644 --- a/plugins/github-actions/src/components/Widget/Widget.tsx +++ b/plugins/github-actions/src/components/Widget/Widget.tsx @@ -15,7 +15,7 @@ */ import React, { useEffect } from 'react'; import { useWorkflowRuns } from '../useWorkflowRuns'; -import { WorkflowRun } from '../WorkflowRunsTable'; +import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable'; import { Entity } from '@backstage/catalog-model'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { @@ -108,3 +108,54 @@ export const Widget = ({ ); }; + +const WidgetListContent = ({ + error, + loading, + branch, +}: { + error?: Error; + loading?: boolean; + lastRun: WorkflowRun; + branch: string; +}) => { + if (error) return Couldn't fetch {branch} runs; + if (loading) return ; + return ; +}; + +export const WidgetList = ({ + entity, + branch = 'master', +}: { + entity: Entity; + branch: string; +}) => { + const errorApi = useApi(errorApiRef); + const [owner, repo] = ( + entity?.metadata.annotations?.['backstage.io/github-actions-id'] ?? '/' + ).split('/'); + const [{ runs, loading, error }] = useWorkflowRuns({ + owner, + repo, + branch, + }); + + const lastRun = runs?.[0] ?? ({} as WorkflowRun); + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + + return ( + + + + ); +}; diff --git a/plugins/github-actions/src/components/Widget/index.ts b/plugins/github-actions/src/components/Widget/index.ts index 2b34671ab5..0efc84d952 100644 --- a/plugins/github-actions/src/components/Widget/index.ts +++ b/plugins/github-actions/src/components/Widget/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Widget } from './Widget'; +export { Widget, WidgetList } from './Widget'; From e2473c6989da81f711fac75f174a6af0fe72c5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 28 Aug 2020 11:22:20 +0200 Subject: [PATCH 38/68] fix(build): remove node_modules caching on windows --- .github/workflows/e2e-win.yml | 8 -------- .github/workflows/master-win.yml | 8 -------- 2 files changed, 16 deletions(-) diff --git a/.github/workflows/e2e-win.yml b/.github/workflows/e2e-win.yml index 37cb13ffbe..5ebf67bc9a 100644 --- a/.github/workflows/e2e-win.yml +++ b/.github/workflows/e2e-win.yml @@ -33,19 +33,11 @@ jobs: with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - name: find location of global yarn cache id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache uses: actions/cache@v2 - if: steps.cache-modules.outputs.cache-hit != 'true' with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} diff --git a/.github/workflows/master-win.yml b/.github/workflows/master-win.yml index b5a042a236..296a7511bc 100644 --- a/.github/workflows/master-win.yml +++ b/.github/workflows/master-win.yml @@ -25,19 +25,11 @@ jobs: with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - name: find location of global yarn cache id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache uses: actions/cache@v2 - if: steps.cache-modules.outputs.cache-hit != 'true' with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} From cd3dde5af6ae3792d297b95d5b450ea39ee9e9f9 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 28 Aug 2020 11:56:22 +0200 Subject: [PATCH 39/68] add techdocs big picture to docs (#2153) --- docs/assets/techdocs/techdocs_big_picture.png | Bin 0 -> 137076 bytes docs/features/techdocs/README.md | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 docs/assets/techdocs/techdocs_big_picture.png diff --git a/docs/assets/techdocs/techdocs_big_picture.png b/docs/assets/techdocs/techdocs_big_picture.png new file mode 100644 index 0000000000000000000000000000000000000000..ffe437180f2df0e8df7bfb8db162b38733f54622 GIT binary patch literal 137076 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

_X92zeSU`dW=JJL~nLKNk+YZU1+ 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 literal 0 HcmV?d00001 diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index c0e6ccfda9..8bdbd27d3c 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -101,3 +101,7 @@ more to come... https://github.com/spotify/backstage/blob/master/packages/techdocs-container [techdocs/cli]: https://github.com/spotify/backstage/blob/master/packages/techdocs-cli + +## TechDocs Big Picture + +![TechDocs Big Picture](../..//assets/techdocs/techdocs_big_picture.png) From a16499065864dc2c09fa8ea2b584e2f8fbaaebcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 27 Aug 2020 11:12:57 +0200 Subject: [PATCH 40/68] fix(create-app): updated the backend template to match new functionality --- app-config.yaml | 5 +++ .../{app-config.yaml => app-config.yaml.hbs} | 27 ++++++++++++++-- .../backend/src/{index.ts.hbs => index.ts} | 31 ++----------------- 3 files changed, 32 insertions(+), 31 deletions(-) rename packages/create-app/templates/default-app/{app-config.yaml => app-config.yaml.hbs} (83%) rename packages/create-app/templates/default-app/packages/backend/src/{index.ts.hbs => index.ts} (73%) diff --git a/app-config.yaml b/app-config.yaml index 38f16e1ac3..0a85198630 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -61,6 +61,11 @@ catalog: - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml + - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml + - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml + - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml + - https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml + - https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml auth: providers: diff --git a/packages/create-app/templates/default-app/app-config.yaml b/packages/create-app/templates/default-app/app-config.yaml.hbs similarity index 83% rename from packages/create-app/templates/default-app/app-config.yaml rename to packages/create-app/templates/default-app/app-config.yaml.hbs index 0c3ec3ed80..a8e1e194a0 100644 --- a/packages/create-app/templates/default-app/app-config.yaml +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -13,6 +13,28 @@ backend: origin: http://localhost:3000 methods: [GET, POST, PUT, DELETE] credentials: true + {{#if dbTypeSqlite}} + database: + client: sqlite3 + connection: ':memory:' + {{/if}} + {{#if dbTypePG}} + database: + client: pg + connection: + host: + $secret: + env: POSTGRES_HOST + port: + $secret: + env: POSTGRES_PORT + user: + $secret: + env: POSTGRES_USER + password: + $secret: + env: POSTGRES_PASSWORD + {{/if}} proxy: '/test': @@ -27,7 +49,7 @@ auth: catalog: locations: - # Backstage Example Component + # Backstage example components - type: github target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml - type: github @@ -44,8 +66,7 @@ catalog: target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml - type: github target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml - - # Backstage Example Templates + # Backstage example templates - type: github target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml - type: github diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts.hbs b/packages/create-app/templates/default-app/packages/backend/src/index.ts similarity index 73% rename from packages/create-app/templates/default-app/packages/backend/src/index.ts.hbs rename to packages/create-app/templates/default-app/packages/backend/src/index.ts index 98f40ba4a4..0e1a85f0a3 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts.hbs +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -7,18 +7,13 @@ */ import { + createDatabase, createServiceBuilder, loadBackendConfig, getRootLogger, useHotMemoize, } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; -{{#if dbTypePG}} -import knex, { PgConnectionConfig } from 'knex'; -{{/if}} -{{#if dbTypeSqlite}} -import knex from 'knex'; -{{/if}} import auth from './plugins/auth'; import catalog from './plugins/catalog'; import identity from './plugins/identity'; @@ -32,30 +27,10 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) { return (plugin: string): PluginEnvironment => { const logger = getRootLogger().child({ type: 'plugin', plugin }); - - {{#if dbTypePG}} - const knexConfig = { - client: 'pg', - useNullAsDefault: true, + const database = createDatabase(config.getConfig('backend.database'), { connection: { - port: process.env.POSTGRES_PORT, - host: process.env.POSTGRES_HOST, - user: process.env.POSTGRES_USER, - password: process.env.POSTGRES_PASSWORD, database: `backstage_plugin_${plugin}`, - } as PgConnectionConfig, - }; - {{/if}} - {{#if dbTypeSqlite}} - const knexConfig = { - client: 'sqlite3', - connection: ':memory:', - useNullAsDefault: true, - }; - {{/if}} - const database = knex(knexConfig); - database.client.pool.on('createSuccess', (_eventId: any, resource: any) => { - resource.run('PRAGMA foreign_keys = ON', () => {}); + }, }); return { logger, database, config }; }; From 8f6038951f6a98b42e0828ccbd8c94c0c92eeae7 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 27 Aug 2020 14:40:22 +0200 Subject: [PATCH 41/68] feat(backend-common): introduce metrics for the backend Add a /metrics endpoint that exposes metrics about the backend. It provides general runtime metrics (memory, gc, utilization) as well as request metrics. --- packages/backend-common/package.json | 2 + .../src/service/lib/ServiceBuilderImpl.ts | 2 + .../backend-common/src/service/lib/metrics.ts | 37 +++++++++++++++++++ yarn.lock | 34 ++++++++++++++++- 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 packages/backend-common/src/service/lib/metrics.ts diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 1a254d891f..92d301d7e3 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -37,11 +37,13 @@ "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", + "express-prom-bundle": "^6.1.0", "express-promise-router": "^3.0.3", "helmet": "^4.0.0", "knex": "^0.21.1", "lodash": "^4.17.15", "morgan": "^1.10.0", + "prom-client": "^12.0.0", "selfsigned": "^1.10.7", "stoppable": "^1.1.0", "winston": "^3.2.1" diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 4669c00e68..e3d48bbabc 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -37,6 +37,7 @@ import { HttpsSettings, } from './config'; import { createHttpServer, createHttpsServer } from './hostFactory'; +import { metricsHandler } from './metrics'; const DEFAULT_PORT = 7000; // '' is express default, which listens to all interfaces @@ -131,6 +132,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { } app.use(compression()); app.use(express.json()); + app.use(metricsHandler()); app.use(requestLoggingHandler()); for (const [root, route] of this.routers) { app.use(root, route); diff --git a/packages/backend-common/src/service/lib/metrics.ts b/packages/backend-common/src/service/lib/metrics.ts new file mode 100644 index 0000000000..d7b544765b --- /dev/null +++ b/packages/backend-common/src/service/lib/metrics.ts @@ -0,0 +1,37 @@ +/* + * 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 prom from 'prom-client'; +import promBundle from 'express-prom-bundle'; +import { RequestHandler } from 'express'; + +/** + * Adds a /metrics endpoint, register default runtime metrics and instrument the router. + */ +export function metricsHandler(): RequestHandler { + // We can only initialize the metrics once and have to clean them up between hot reloads + prom.register.clear(); + + return promBundle({ + includeMethod: true, + includePath: true, + // Using includePath alone is problematic, as it will include path labels with high + // cardinality (e.g. path params). Instead we would have to template them. However, this + // is difficult, as every backend plugin might use different routes. Instead we only take + // the first directory of the path, to have at least an idea how each plugin performs: + normalizePath: [['^/([^/]*)/.*', '/$1']], + promClient: { collectDefaultMetrics: {} }, + }); +} diff --git a/yarn.lock b/yarn.lock index d56a2cbd2c..59111a320e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6511,6 +6511,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bintrees@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" + integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= + bl@^1.0.0: version "1.2.2" resolved "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" @@ -10135,6 +10140,14 @@ expect@^26.0.1: jest-message-util "^26.0.1" jest-regex-util "^26.0.0" +express-prom-bundle@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.1.0.tgz#8fd72e5bedbbd686b8e7c49e0aecbc1b14b28743" + integrity sha512-krlvp5r6sgJ1IwL6M6/coMrNbAlwtpk+uyivfeyRMCupTK4HzIEQHH0gwrNhLiKyPmSbtZcSmtO6s+PRumRp5g== + dependencies: + on-finished "^2.3.0" + url-value-parser "^2.0.0" + express-promise-router@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-3.0.3.tgz#5e6d22a5a3f013d71833172fe8d7ab780c3f6b70" @@ -16227,7 +16240,7 @@ omggif@1.0.7: resolved "https://registry.npmjs.org/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d" integrity sha1-WdLuywJj3oRjWz/riHwMmXPx5J0= -on-finished@~2.3.0: +on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= @@ -17809,6 +17822,13 @@ progress@^2.0.0: resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +prom-client@^12.0.0: + version "12.0.0" + resolved "https://registry.npmjs.org/prom-client/-/prom-client-12.0.0.tgz#9689379b19bd3f6ab88a9866124db9da3d76c6ed" + integrity sha512-JbzzHnw0VDwCvoqf8y1WDtq4wSBAbthMB1pcVI/0lzdqHGJI3KBJDXle70XK+c7Iv93Gihqo0a5LlOn+g8+DrQ== + dependencies: + tdigest "^0.1.1" + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -21142,6 +21162,13 @@ tcp-port-used@^1.0.1: debug "4.1.0" is2 "2.0.1" +tdigest@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" + integrity sha1-Ljyyw56kSeVdHmzZEReszKRYgCE= + dependencies: + bintrees "1.0.1" + telejson@^3.2.0: version "3.3.0" resolved "https://registry.npmjs.org/telejson/-/telejson-3.3.0.tgz#6d814f3c0d254d5c4770085aad063e266b56ad03" @@ -22131,6 +22158,11 @@ url-to-options@^1.0.1: resolved "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= +url-value-parser@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.0.1.tgz#c8179a095ab9ec1f5aa17ca36af5af396b4e95ed" + integrity sha512-bexECeREBIueboLGM3Y1WaAzQkIn+Tca/Xjmjmfd0S/hFHSCEoFkNh0/D0l9G4K74MkEP/lLFRlYnxX3d68Qgw== + url@^0.11.0, url@~0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" From f5c0aadbe4d10a905057cdfc90880b746c0d45d8 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 27 Aug 2020 16:36:12 +0200 Subject: [PATCH 42/68] feat(backend-common): make metrics configurable --- .../backend-common/src/service/lib/ServiceBuilderImpl.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index e3d48bbabc..df69387e40 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -49,6 +49,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { private logger: Logger | undefined; private corsOptions: cors.CorsOptions | undefined; private httpsSettings: HttpsSettings | undefined; + private enableMetrics: boolean = true; private routers: [string, Router][]; // Reference to the module where builder is created - needed for hot module // reloading @@ -83,6 +84,9 @@ export class ServiceBuilderImpl implements ServiceBuilder { this.httpsSettings = httpsSettings; } + // For now, configuration of metrics is a simple boolean and active by default + this.enableMetrics = backendConfig.getOptionalBoolean('metrics') !== false; + return this; } @@ -132,7 +136,9 @@ export class ServiceBuilderImpl implements ServiceBuilder { } app.use(compression()); app.use(express.json()); - app.use(metricsHandler()); + if (this.enableMetrics) { + app.use(metricsHandler()); + } app.use(requestLoggingHandler()); for (const [root, route] of this.routers) { app.use(root, route); From a7fdb17356fccde5f98e15bf144c3ad775cd249d Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 20 Aug 2020 15:53:28 +0200 Subject: [PATCH 43/68] refactor(core): change SidebarUserSettings to accept a list of providerSettings This allows to configure custom provider settings without the need to modify the core package. --- packages/app/src/components/Root/Root.tsx | 3 +- .../Sidebar/DefaultProviderSettings.tsx | 81 +++++++++++++++++++ .../src/layout/Sidebar/Sidebar.stories.tsx | 13 ++- .../core/src/layout/Sidebar/UserSettings.tsx | 72 +++-------------- packages/core/src/layout/Sidebar/index.ts | 1 + .../default-app/packages/app/src/sidebar.tsx | 3 +- 6 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 1d32b6e16e..d0a0171207 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -39,6 +39,7 @@ import { SidebarUserSettings, SidebarThemeToggle, SidebarPinButton, + DefaultProviderSettings, } from '@backstage/core'; import { NavLink } from 'react-router-dom'; import { graphiQLRouteRef } from '@backstage/plugin-graphiql'; @@ -107,7 +108,7 @@ const Root: FC<{}> = ({ children }) => ( - + } /> {children} diff --git a/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx b/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx new file mode 100644 index 0000000000..293cefb19a --- /dev/null +++ b/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx @@ -0,0 +1,81 @@ +/* + * 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 { + configApiRef, + githubAuthApiRef, + gitlabAuthApiRef, + googleAuthApiRef, + oauth2ApiRef, + oktaAuthApiRef, + microsoftAuthApiRef, + useApi, +} from '@backstage/core-api'; +import Star from '@material-ui/icons/Star'; +import React from 'react'; +import { OAuthProviderSettings, OIDCProviderSettings } from './Settings'; + +export const DefaultProviderSettings = () => { + const configApi = useApi(configApiRef); + const providersConfig = configApi.getOptionalConfig('auth.providers'); + const providers = providersConfig?.keys() ?? []; + + return ( + <> + {providers.includes('google') && ( + + )} + {providers.includes('microsoft') && ( + + )} + {providers.includes('github') && ( + + )} + {providers.includes('gitlab') && ( + + )} + {providers.includes('okta') && ( + + )} + {providers.includes('oauth2') && ( + + )} + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx index 906317fe9c..b286071426 100644 --- a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx +++ b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx @@ -23,10 +23,15 @@ import { SidebarSearchField, SidebarSpace, SidebarUserSettings, + OAuthProviderSettings, } from '.'; import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined'; import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; +import Star from '@material-ui/icons/Star'; import { MemoryRouter } from 'react-router-dom'; +import { + githubAuthApiRef, +} from '@backstage/core-api'; export default { title: 'Sidebar', @@ -55,6 +60,12 @@ export const SampleSidebar = () => ( - + + } /> ); diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 5ea7d2b359..f586f6d077 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -14,36 +14,22 @@ * limitations under the License. */ -import { - githubAuthApiRef, - gitlabAuthApiRef, - googleAuthApiRef, - identityApiRef, - oauth2ApiRef, - oktaAuthApiRef, - microsoftAuthApiRef, - useApi, - configApiRef, -} from '@backstage/core-api'; +import { identityApiRef, useApi } from '@backstage/core-api'; import Collapse from '@material-ui/core/Collapse'; import SignOutIcon from '@material-ui/icons/MeetingRoom'; -import Star from '@material-ui/icons/Star'; import React, { useContext, useEffect } from 'react'; import { SidebarContext } from './config'; import { SidebarItem } from './Items'; -import { - OAuthProviderSettings, - OIDCProviderSettings, - UserProfile as SidebarUserProfile, -} from './Settings'; +import { UserProfile as SidebarUserProfile } from './Settings'; -export function SidebarUserSettings() { +type SidebarUserSettingsProps = { providerSettings?: React.ReactNode }; + +export function SidebarUserSettings({ + providerSettings, +}: SidebarUserSettingsProps) { const { isOpen: sidebarOpen } = useContext(SidebarContext); const [open, setOpen] = React.useState(false); const identityApi = useApi(identityApiRef); - const configApi = useApi(configApiRef); - const providersConfig = configApi.getOptionalConfig('auth.providers'); - const providers = providersConfig?.keys() ?? []; // Close the provider list when sidebar collapse useEffect(() => { @@ -54,48 +40,8 @@ export function SidebarUserSettings() { <> - {providers.includes('google') && ( - - )} - {providers.includes('microsoft') && ( - - )} - {providers.includes('github') && ( - - )} - {providers.includes('gitlab') && ( - - )} - {providers.includes('okta') && ( - - )} - {providers.includes('oauth2') && ( - - )} + {providerSettings} + ( @@ -22,7 +23,7 @@ export const AppSidebar = () => ( - + } /> ); From 1cbc3c62693f8fee783e29283ef0ff50da6e284f Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 28 Aug 2020 13:58:05 +0200 Subject: [PATCH 44/68] Rename function WidgetList to RecentWorkflowRunsCard and WidgetListContet to RecentWorkflowRunsCardContent. Also remove lastrun parameter that was not beeing used on the RecentWorkflowRunsCardContent function --- .../github-actions/src/components/Widget/Widget.tsx | 11 ++++------- plugins/github-actions/src/components/Widget/index.ts | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/plugins/github-actions/src/components/Widget/Widget.tsx b/plugins/github-actions/src/components/Widget/Widget.tsx index 201004f46e..59e1cc5289 100644 --- a/plugins/github-actions/src/components/Widget/Widget.tsx +++ b/plugins/github-actions/src/components/Widget/Widget.tsx @@ -109,14 +109,13 @@ export const Widget = ({ ); }; -const WidgetListContent = ({ +const RecentWorkflowRunsCardContent = ({ error, loading, branch, }: { error?: Error; loading?: boolean; - lastRun: WorkflowRun; branch: string; }) => { if (error) return Couldn't fetch {branch} runs; @@ -124,7 +123,7 @@ const WidgetListContent = ({ return ; }; -export const WidgetList = ({ +export const RecentWorkflowRunsCard = ({ entity, branch = 'master', }: { @@ -135,13 +134,12 @@ export const WidgetList = ({ const [owner, repo] = ( entity?.metadata.annotations?.['backstage.io/github-actions-id'] ?? '/' ).split('/'); - const [{ runs, loading, error }] = useWorkflowRuns({ + const [{ loading, error }] = useWorkflowRuns({ owner, repo, branch, }); - const lastRun = runs?.[0] ?? ({} as WorkflowRun); useEffect(() => { if (error) { errorApi.post(error); @@ -150,11 +148,10 @@ export const WidgetList = ({ return ( - ); diff --git a/plugins/github-actions/src/components/Widget/index.ts b/plugins/github-actions/src/components/Widget/index.ts index 0efc84d952..0bd5dad98a 100644 --- a/plugins/github-actions/src/components/Widget/index.ts +++ b/plugins/github-actions/src/components/Widget/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Widget, WidgetList } from './Widget'; +export { Widget, RecentWorkflowRunsCard } from './Widget'; From 6d774764cd6e444cfe672af46478ca28781fd92c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 26 Aug 2020 16:18:21 +0200 Subject: [PATCH 45/68] feat(proxy-backend): more proxy config tweaks and docs --- app-config.yaml | 12 ++--- packages/backend/src/index.ts | 2 +- packages/backend/src/plugins/proxy.ts | 11 +++-- plugins/proxy-backend/README.md | 46 ++++++++++++++++++- .../proxy-backend/src/service/router.test.ts | 1 + plugins/proxy-backend/src/service/router.ts | 42 +++++++++++++++-- .../src/service/standaloneServer.ts | 1 + 7 files changed, 95 insertions(+), 20 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 0a85198630..2efe6adc32 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -14,21 +14,15 @@ backend: client: sqlite3 connection: ':memory:' +# See README.md in the proxy-backend plugin for information on the configuration format proxy: - '/circleci/api': - target: 'https://circleci.com/api/v1.1' - changeOrigin: true - pathRewrite: - '^/proxy/circleci/api/': '/' + '/circleci/api': https://circleci.com/api/v1.1 '/jenkins/api': - target: 'http://localhost:8080' - changeOrigin: true + target: http://localhost:8080 headers: Authorization: $secret: env: JENKINS_BASIC_AUTH_HEADER - pathRewrite: - '^/proxy/jenkins/api/': '/' organization: name: Spotify diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 50f59795fc..91b1af06fb 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -82,7 +82,7 @@ async function main() { .addRouter('/auth', await auth(authEnv)) .addRouter('/identity', await identity(identityEnv)) .addRouter('/techdocs', await techdocs(techdocsEnv)) - .addRouter('/proxy', await proxy(proxyEnv)) + .addRouter('/proxy', await proxy(proxyEnv, '/proxy')) .addRouter('/graphql', await graphql(graphqlEnv)); await service.start().catch(err => { diff --git a/packages/backend/src/plugins/proxy.ts b/packages/backend/src/plugins/proxy.ts index 4964de130e..e96acf69d3 100644 --- a/packages/backend/src/plugins/proxy.ts +++ b/packages/backend/src/plugins/proxy.ts @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + // @ts-ignore import { createRouter } from '@backstage/plugin-proxy-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin({ - logger, - config, -}: PluginEnvironment) { - return await createRouter({ logger, config }); +export default async function createPlugin( + { logger, config }: PluginEnvironment, + pathPrefix: string, +) { + return await createRouter({ logger, config, pathPrefix }); } diff --git a/plugins/proxy-backend/README.md b/plugins/proxy-backend/README.md index d12031f495..baa2709820 100644 --- a/plugins/proxy-backend/README.md +++ b/plugins/proxy-backend/README.md @@ -19,8 +19,8 @@ const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); const service = createServiceBuilder(module) .loadConfig(configReader) - /** several different routers */ - .addRouter('/', await proxy(proxyEnv)); + /** ... other routers ... */ + .addRouter('/proxy', await proxy(proxyEnv, '/proxy')); ``` 2. Start the backend @@ -31,6 +31,48 @@ yarn workspace example-backend start This will launch the full example backend. +## Configuration + +Example: + +```yaml +# in app-config.yaml +proxy: + '/simple-example': http://simple.example.com:8080 + '/larger-example/v1': + target: http://larger.example.com:8080/svc.v1 + headers: + Authorization: + $secret: + env: EXAMPLE_AUTH_HEADER +``` + +Each key under the proxy configuration entry is a route to match, below the prefix that the proxy +plugin is mounted on. For example, if the backend mounts the proxy plugin as `/proxy`, +the above configuration will lead to the proxy acting on backend requests to +`/proxy/simple-example/...` and `/proxy/larger-example/v1/...`. + +The value inside each route is either a simple URL string, or an object on the format accepted by +[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). + +If the value is a string, it is assumed to correspond to: + +```yaml +target: +changeOrigin: true +pathRewrite: + '^/': '/' +``` + +When the target is an object, it is given verbatim to `http-proxy-middleware` except with the +following caveats for convenience: + +- If `changeOrigin` is not specified, it is set to `true`. This is the most commonly useful value. +- If `pathRewrite` is not specified, it is set to a single rewrite that removes the entire route. In the + above example, a rewrite of `'^/proxy/larger-example/v1/': '/'` is added. That means that a request to + `/proxy/larger-example/v1/some/path` will be translated to a request to + `http://larger.example.com:8080/svc.v1/some/path`. + ## Links - [http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware) diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index 085647bc4e..c7d7fff1d4 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -26,6 +26,7 @@ describe('createRouter', () => { const router = await createRouter({ config, logger, + pathPrefix: '/proxy', }); expect(router).toBeDefined(); }); diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index b71299ef7e..5273b2c806 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -17,21 +17,57 @@ import { Config } from '@backstage/config'; import express from 'express'; import Router from 'express-promise-router'; -import createProxyMiddleware from 'http-proxy-middleware'; +import createProxyMiddleware, { + Config as ProxyConfig, + Proxy, +} from 'http-proxy-middleware'; import { Logger } from 'winston'; export interface RouterOptions { logger: Logger; config: Config; + // The URL path prefix that the router itself is mounted as, commonly "/proxy" + pathPrefix: string; +} + +// Creates a proxy middleware, possibly with defaults added on top of the +// given config. +function buildMiddleware( + pathPrefix: string, + route: string, + config: string | ProxyConfig, +): Proxy { + const fullConfig = + typeof config === 'string' ? { target: config } : { ...config }; + + // Default is to do a path rewrite that strips out the proxy's path prefix + // and the rest of the route. + if (fullConfig.pathRewrite === undefined) { + const routeWithSlash = route.endsWith('/') ? route : `${route}/`; + fullConfig.pathRewrite = { + [`^${pathPrefix}${routeWithSlash}`]: '/', + }; + } + + // Default is to update the Host header to the target + if (fullConfig.changeOrigin === undefined) { + fullConfig.changeOrigin = true; + } + + return createProxyMiddleware(fullConfig); } export async function createRouter( options: RouterOptions, ): Promise { const router = Router(); - const proxyConfig = options.config.get('proxy') ?? {}; + + const proxyConfig = options.config.getOptional('proxy') ?? {}; Object.entries(proxyConfig).forEach(([route, proxyRouteConfig]) => { - router.use(route, createProxyMiddleware(proxyRouteConfig)); + router.use( + route, + buildMiddleware(options.pathPrefix, route, proxyRouteConfig), + ); }); return router; diff --git a/plugins/proxy-backend/src/service/standaloneServer.ts b/plugins/proxy-backend/src/service/standaloneServer.ts index f75c044f0f..68e9ade183 100644 --- a/plugins/proxy-backend/src/service/standaloneServer.ts +++ b/plugins/proxy-backend/src/service/standaloneServer.ts @@ -40,6 +40,7 @@ export async function startStandaloneServer( const router = await createRouter({ config, logger, + pathPrefix: '/proxy', }); const service = createServiceBuilder(module) .enableCors({ origin: 'http://localhost:3000' }) From b2f62347e8ebb80cee183931de34cedc04d4b65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 27 Aug 2020 09:26:06 +0200 Subject: [PATCH 46/68] fix tests --- .../default-app/packages/backend/src/index.ts | 2 +- .../default-app/packages/backend/src/plugins/proxy.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index 0e1a85f0a3..d6fba2478a 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -55,7 +55,7 @@ async function main() { .addRouter('/auth', await auth(authEnv)) .addRouter('/identity', await identity(identityEnv)) .addRouter('/techdocs', await techdocs(techdocsEnv)) - .addRouter('/proxy', await proxy(proxyEnv)); + .addRouter('/proxy', await proxy(proxyEnv, '/proxy')); await service.start().catch(err => { console.log(err); diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/proxy.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/proxy.ts index 574d0c17a7..b20265c2cd 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/proxy.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/proxy.ts @@ -2,9 +2,9 @@ import { createRouter } from '@backstage/plugin-proxy-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin({ - logger, - config, -}: PluginEnvironment) { - return await createRouter({ logger, config }); +export default async function createPlugin( + { logger, config }: PluginEnvironment, + pathPrefix: string, +) { + return await createRouter({ logger, config, pathPrefix }); } From 9a64135bf7d66642da35b574070828e2d7d0c0a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 27 Aug 2020 12:06:53 +0200 Subject: [PATCH 47/68] move docs to the microsite --- docs/plugins/call-existing-api.md | 11 ++--- docs/plugins/proxying.md | 71 ++++++++++++++++++++++++++++++- plugins/proxy-backend/README.md | 66 +++++----------------------- 3 files changed, 83 insertions(+), 65 deletions(-) diff --git a/docs/plugins/call-existing-api.md b/docs/plugins/call-existing-api.md index f6c9990b90..9658176224 100644 --- a/docs/plugins/call-existing-api.md +++ b/docs/plugins/call-existing-api.md @@ -71,11 +71,7 @@ Example: ```yaml # In app-config.yaml proxy: - '/frobs': - target: 'http://api.frobsco.com/v1' - changeOrigin: true - pathRewrite: - '^/proxy/frobs/': '/' + '/frobs': http://api.frobsco.com/v1 ``` ```ts @@ -86,9 +82,8 @@ fetch(`${backendUrl}/proxy/frobs/list`) .then(payload => setFrobs(payload as Frob[])); ``` -The proxy is powered by the `http-proxy-middleware` package, and supports all of -its -[configuration options](https://github.com/chimurai/http-proxy-middleware#options). +The proxy is powered by the `http-proxy-middleware` package. See +[Proxying](proxying.md) for a full description of its configuration options. Internally at Spotify, the proxy option has been the overwhelmingly most popular choice for plugin makers. Since we have DNS based service discovery in place and diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md index 4cf5b4c025..658df3dda1 100644 --- a/docs/plugins/proxying.md +++ b/docs/plugins/proxying.md @@ -3,4 +3,73 @@ id: proxying title: Proxying --- -## TODO +## Overview + +The Backstage backend comes packaged with a basic HTTP proxy, that can aid in +reaching backend service APIs from frontend plugin code. See +[Call Existing API](call-existing-api.md) for a description of when the proxy +can be the best choice for communicating with an API. + +## Getting Started + +The plugin is already added to a default Backstage project. + +In `packages/backend/src/index.ts`: + +```ts +const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); + +const service = createServiceBuilder(module) + .loadConfig(configReader) + /** ... other routers ... */ + .addRouter('/proxy', await proxy(proxyEnv, '/proxy')); +``` + +## Configuration + +Configuration for the proxy plugin lives under a `proxy` root key of your +`app-config.yaml` file. + +Example: + +```yaml +# in app-config.yaml +proxy: + '/simple-example': http://simple.example.com:8080 + '/larger-example/v1': + target: http://larger.example.com:8080/svc.v1 + headers: + Authorization: + $secret: + env: EXAMPLE_AUTH_HEADER +``` + +Each key under the proxy configuration entry is a route to match, below the +prefix that the proxy plugin is mounted on. It must start with a slash. For +example, if the backend mounts the proxy plugin as `/proxy`, the above +configuration will lead to the proxy acting on backend requests to +`/proxy/simple-example/...` and `/proxy/larger-example/v1/...`. + +The value inside each route is either a simple URL string, or an object on the +format accepted by +[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). + +If the value is a string, it is assumed to correspond to: + +```yaml +target: +changeOrigin: true +pathRewrite: + '^/': '/' +``` + +When the target is an object, it is given verbatim to `http-proxy-middleware` +except with the following caveats for convenience: + +- If `changeOrigin` is not specified, it is set to `true`. This is the most + commonly useful value. +- If `pathRewrite` is not specified, it is set to a single rewrite that removes + the entire prefix and route. In the above example, a rewrite of + `'^/proxy/larger-example/v1/': '/'` is added. That means that a request to + `/proxy/larger-example/v1/some/path` will be translated to a request to + `http://larger.example.com:8080/svc.v1/some/path`. diff --git a/plugins/proxy-backend/README.md b/plugins/proxy-backend/README.md index baa2709820..f67449dda3 100644 --- a/plugins/proxy-backend/README.md +++ b/plugins/proxy-backend/README.md @@ -1,6 +1,7 @@ # Proxy backend plugin -This is the backend plugin that enables proxy definitions to be declared in and read from app-config.yaml. +This is the backend plugin that enables proxy definitions to be declared in, +and read from, `app-config.yaml`. Relies on the `http-proxy-middleware` package. @@ -10,69 +11,22 @@ This backend plugin can be started in a standalone mode from directly in this pa with `yarn start`. However, it will have limited functionality and that process is most convenient when developing the plugin itself. -To run it within the backend do: - -1. Register the router in `packages/backend/src/index.ts`: - -```ts -const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); - -const service = createServiceBuilder(module) - .loadConfig(configReader) - /** ... other routers ... */ - .addRouter('/proxy', await proxy(proxyEnv, '/proxy')); -``` - -2. Start the backend +The proxy is already installed in the Backstage backend per default, so you can also +start up the full example backend to experiment with the proxy. ```bash yarn workspace example-backend start ``` -This will launch the full example backend. - ## Configuration -Example: - -```yaml -# in app-config.yaml -proxy: - '/simple-example': http://simple.example.com:8080 - '/larger-example/v1': - target: http://larger.example.com:8080/svc.v1 - headers: - Authorization: - $secret: - env: EXAMPLE_AUTH_HEADER -``` - -Each key under the proxy configuration entry is a route to match, below the prefix that the proxy -plugin is mounted on. For example, if the backend mounts the proxy plugin as `/proxy`, -the above configuration will lead to the proxy acting on backend requests to -`/proxy/simple-example/...` and `/proxy/larger-example/v1/...`. - -The value inside each route is either a simple URL string, or an object on the format accepted by -[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). - -If the value is a string, it is assumed to correspond to: - -```yaml -target: -changeOrigin: true -pathRewrite: - '^/': '/' -``` - -When the target is an object, it is given verbatim to `http-proxy-middleware` except with the -following caveats for convenience: - -- If `changeOrigin` is not specified, it is set to `true`. This is the most commonly useful value. -- If `pathRewrite` is not specified, it is set to a single rewrite that removes the entire route. In the - above example, a rewrite of `'^/proxy/larger-example/v1/': '/'` is added. That means that a request to - `/proxy/larger-example/v1/some/path` will be translated to a request to - `http://larger.example.com:8080/svc.v1/some/path`. +See [the proxy docs](https://backstage.io/docs/plugins/proxying). ## Links +- [Call Existing API](https://backstage.io/docs/plugins/call-existing-api) helps the + decision process of what method of communication to use from a frontend plugin to + your API +- [The proxy plugin documentation](https://backstage.io/docs/plugins/proxying) describes + configuration options and more - [http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware) From 86aff7a6cfd87bd8c8ef5b46d7fac57007a13fc0 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 28 Aug 2020 14:39:36 +0200 Subject: [PATCH 48/68] Added github as CI/CD in catalog plugin --- plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx | 2 +- plugins/github-actions/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx index 01de6dc9a2..d39617da25 100644 --- a/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx +++ b/plugins/catalog/src/components/EntityPageCi/EntityPageCi.tsx @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { Content } from '@backstage/core'; -import { WidgetList as GithubActionsListWidget } from '@backstage/plugin-github-actions'; +import { RecentWorkflowRunsCard as GithubActionsListWidget } from '@backstage/plugin-github-actions'; import { Grid } from '@material-ui/core'; import React, { FC } from 'react'; diff --git a/plugins/github-actions/src/index.ts b/plugins/github-actions/src/index.ts index 4a69c363cd..4e383fdd3d 100644 --- a/plugins/github-actions/src/index.ts +++ b/plugins/github-actions/src/index.ts @@ -16,4 +16,4 @@ export { plugin } from './plugin'; export * from './api'; -export { Widget } from './components/Widget'; +export { Widget, RecentWorkflowRunsCard } from './components/Widget'; From 63339d2692b550035fa26b7e680a03d66987acc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 28 Aug 2020 14:50:41 +0200 Subject: [PATCH 49/68] chore(register-component): just a few little tweaks --- .../RegisterComponentForm.test.tsx | 6 +++--- .../RegisterComponentForm/RegisterComponentForm.tsx | 10 +++++----- plugins/register-component/src/util/validate.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx index e1226bbf32..bb7faabc9d 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ +import { cleanup, fireEvent, render } from '@testing-library/react'; import React from 'react'; -import { render, fireEvent, cleanup } from '@testing-library/react'; -import RegisterComponentForm, { Props } from './RegisterComponentForm'; import { act } from 'react-dom/test-utils'; +import RegisterComponentForm, { Props } from './RegisterComponentForm'; const setup = (props?: Partial) => { return { @@ -37,7 +37,7 @@ describe('RegisterComponentForm', () => { const { rendered } = setup(); expect( await rendered.findByText( - 'Enter the full path to the component.yaml file in GitHub, GitLab, Bitbucket or Azure to start tracking your component. For private repo provide authentication information via config.', + 'Enter the full path to the component.yaml file in GitHub, GitLab, Bitbucket or Azure to start tracking your component.', ), ).toBeInTheDocument(); diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx index de3c54610d..1b84b79234 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx @@ -14,17 +14,17 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import { BackstageTheme } from '@backstage/theme'; import { Button, FormControl, FormHelperText, - TextField, LinearProgress, + TextField, } from '@material-ui/core'; -import { useForm } from 'react-hook-form'; import { makeStyles } from '@material-ui/core/styles'; -import { BackstageTheme } from '@backstage/theme'; +import React, { FC } from 'react'; +import { useForm } from 'react-hook-form'; import { ComponentIdValidators } from '../../util/validate'; const useStyles = makeStyles(theme => ({ @@ -71,7 +71,7 @@ const RegisterComponentForm: FC = ({ onSubmit, submitting }) => { name="componentLocation" required margin="normal" - helperText="Enter the full path to the component.yaml file in GitHub, GitLab, Bitbucket or Azure to start tracking your component. For private repo provide authentication information via config." + helperText="Enter the full path to the component.yaml file in GitHub, GitLab, Bitbucket or Azure to start tracking your component." inputRef={register({ required: true, validate: ComponentIdValidators, diff --git a/plugins/register-component/src/util/validate.ts b/plugins/register-component/src/util/validate.ts index 8552872015..9afb5ebd97 100644 --- a/plugins/register-component/src/util/validate.ts +++ b/plugins/register-component/src/util/validate.ts @@ -19,6 +19,6 @@ export const ComponentIdValidators = { (typeof value === 'string' && value.match(/^https:\/\//) !== null) || 'Must start with https://.', yamlValidator: (value: any) => - (typeof value === 'string' && value.match(/.yaml/) !== null) || + (typeof value === 'string' && value.match(/\.yaml/) !== null) || "Must contain '.yaml'.", }; From 92d6f9bacffe019bc13024960c0f6ea846d490a0 Mon Sep 17 00:00:00 2001 From: Ashok Manji <1902568+ashokm@users.noreply.github.com> Date: Fri, 28 Aug 2020 15:31:29 +0200 Subject: [PATCH 50/68] Fix typo on Backstage Helm Chart README --- install/kubernetes/backstage/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/kubernetes/backstage/README.md b/install/kubernetes/backstage/README.md index e8d809c228..fc3d20733b 100644 --- a/install/kubernetes/backstage/README.md +++ b/install/kubernetes/backstage/README.md @@ -23,7 +23,7 @@ | app.resources | Kubernetes Pod resource requests/limits | `{}` | | app.nodeSelector | Node selectors for scheduling app/frontend pods | `{}` | | app.tolerations | Tolerations for scheduling app/frontend pods | `{}` | -| app.affinity | Affinity setttings for scheduling app/frontend pods | `{}` | +| app.affinity | Affinity settings for scheduling app/frontend pods | `{}` | ## Backend Values @@ -48,4 +48,4 @@ | backend.resources | Kubernetes Pod resource requests/limits | `{}` | | backend.nodeSelector | Node selectors for scheduling backend pods | `{}` | | backend.tolerations | Tolerations for scheduling backend pods | `{}` | -| backend.affinity | Affinity setttings for scheduling backend pods | `{}` | +| backend.affinity | Affinity settings for scheduling backend pods | `{}` | From 314eeb31d77897603791c2331a4719cac8bd4a38 Mon Sep 17 00:00:00 2001 From: Jesse Alan Johnson Date: Fri, 28 Aug 2020 11:57:04 -0400 Subject: [PATCH 51/68] documentation update (#2147) --- docs/getting-started/index.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index bfd3c70913..abe7776371 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -3,9 +3,22 @@ id: index title: Running Backstage Locally --- +First make sure you are using NodeJS with an Active LTS Release, currently v12. +This is made easy with a version manager such as nvm which allows for version switching. + +```bash +# Checking your version +node --version +> v14.7.0 + +# Adding a second node version +nvm install 12 +> Downloading and installing node v12.18.3... +> Now using node v12.18.3 (npm v6.14.6) +``` + To get up and running with a local Backstage to evaluate it, let's clone it off -of GitHub and run an initial build. First make sure that you have at least node -version 12 installed locally. +of GitHub and run an initial build. ```bash # Start from your local development folder From 57c3e2ad5baf30f6b2bc732ad79492e997c7af9f Mon Sep 17 00:00:00 2001 From: Rod Machen Date: Fri, 28 Aug 2020 14:07:25 -0500 Subject: [PATCH 52/68] update software template docs --- .../software-templates/adding-templates.md | 6 +++--- .../extending/create-your-own-publisher.md | 6 +++--- .../extending/create-your-own-templater.md | 12 +++++------ .../software-templates/extending/index.md | 20 +++++++++---------- docs/features/software-templates/index.md | 12 +++++------ .../src/scaffolder/stages/templater/types.ts | 2 +- .../src/techdocs/stages/generate/types.ts | 2 +- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index 67e9c23605..67467bd98e 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -58,7 +58,7 @@ Currently the catalog supports loading definitions from GitHub + Local Files. To load from other places, not only will there need to be another preparer, but the support to load the location will also need to be added to the Catalog. -For loading from a file the following command should work when the backend is +For loading from a file, the following command should work when the backend is running: ```sh @@ -69,7 +69,7 @@ curl \ --data-raw "{\"type\": \"file\", \"target\": \"${YOUR PATH HERE}/template.yaml\"}" ``` -If loading from a git location, you can run the following +If loading from a Git location, you can run the following ```sh curl \ @@ -83,7 +83,7 @@ This should then have added the catalog, and also should now be listed under the create page at http://localhost:3000/create. Alternatively, if you want to get setup with some mock templates that are -already provided for you, you can run the following to load those templates: +already provided, run the following to load those templates: ``` yarn lerna run mock-data diff --git a/docs/features/software-templates/extending/create-your-own-publisher.md b/docs/features/software-templates/extending/create-your-own-publisher.md index 51869dc3ad..391f6f0658 100644 --- a/docs/features/software-templates/extending/create-your-own-publisher.md +++ b/docs/features/software-templates/extending/create-your-own-publisher.md @@ -7,8 +7,8 @@ Publishers are responsible for pushing and storing the templated skeleton after the values have been templated by the `Templater`. See [Create your own templater](./create-your-own-templater.md) for more info. -They recieve a directory or location where the templater has sucessfully run on, -and is now ready to store somewhere. They also get given some other options +They receive a directory or location where the templater has sucessfully run +and is now ready to store somewhere. They also are given some other options which are sent from the frontend, such as the `storePath` which is a string of where the frontend thinks we should save this templated folder. @@ -17,7 +17,7 @@ Currently we provide the following `publishers`: - `github` This publisher is passed through to the `createRouter` function of the -`@spotify/plugin-scaffolder-backend`. Currently only one publisher is supported, +`@spotify/plugin-scaffolder-backend`. Currently, only one publisher is supported, but PR's are always welcome. An full example backend can be found diff --git a/docs/features/software-templates/extending/create-your-own-templater.md b/docs/features/software-templates/extending/create-your-own-templater.md index a39f87924b..9dee66af06 100644 --- a/docs/features/software-templates/extending/create-your-own-templater.md +++ b/docs/features/software-templates/extending/create-your-own-templater.md @@ -8,14 +8,14 @@ returned by the preparers, and then executing the templating command on top of the file and returning the completed template path. This may or may not be the same directory as the input directory. -They also recieve additional values from the frontend, which can be used to +They also receive additional values from the frontend, which can be used to interpolate into the skeleton files. Currently we provide the following templaters: - `cookiecutter` -This templater is added the `TemplaterBuilder` and then passed into the +This templater is added to the `TemplaterBuilder` and then passed into the `createRouter` function of the `@spotify/plugin-scaffolder-backend` An full example backend can be found @@ -48,7 +48,7 @@ This `TemplaterKey` is used to select the correct templater from the `spec.templater` in the [Template Entity](../../software-catalog/descriptor-format.md#kind-template). -If you wish to add a new templater you'll need to register it with the +If you wish to add a new templater, you'll need to register it with the `TemplaterBuilder`. ### Creating your own Templater to add to the `TemplaterBuilder` @@ -83,10 +83,10 @@ follows: - `dockerClient` - a [dockerode](https://github.com/apocas/dockerode) client to be able to run docker containers. -_note_ currently the templaters that we provide are basically docker action +_note_ Currently the templaters that we provide are basically Docker action containers that are run on top of the skeleton folder. This keeps dependencies to a minimal for running backstage scaffolder, but you don't /have/ to use -docker. You could create your own templater that spins up an EC2 instance and +Docker. You could create your own templater that spins up an EC2 instance and downloads the folder and does everything using an AMI if you want. It's entirely up to you! @@ -138,7 +138,7 @@ spec: description: Description of the component ``` -You see that the `spec.templater` is set as `handlebars`, you'll need to +You see that the `spec.templater` is set as `handlebars`, so you'll need to register this with the `TemplaterBuilder` like so: ```ts diff --git a/docs/features/software-templates/extending/index.md b/docs/features/software-templates/extending/index.md index 21d8befda1..02d27a5725 100644 --- a/docs/features/software-templates/extending/index.md +++ b/docs/features/software-templates/extending/index.md @@ -5,11 +5,11 @@ title: Extending the Scaffolder Welcome. Take a seat. You're at the Scaffolder Documentation. -So - You wanna create stuff inside your company from some prebaked templates? +So, you want to create stuff inside your company from some prebaked templates? You're at the right place. -This guide is gonna take you through how the Scaffolder in Backstage works. -We'll dive into some jargon and run through whats going on in the backend to be +This guide is going to take you through how the Scaffolder in Backstage works. +We'll dive into some jargon and run through what's going on in the backend to be able to create these templates. There's also more guides that you might find useful at the bottom of this document. At it's core, theres 3 simple stages. @@ -25,9 +25,9 @@ scaffolder that you will need to know: 3. Publish Each of these steps can be configured for your own use case, but we provide some -sensible defaults too. +sensible defaults, too. -Lets dive a little deeper into these phases. +Let's dive a little deeper into these phases. ### Glossary and Jargon @@ -38,8 +38,8 @@ the router to pick the correct `Preparer` to run for the `Template` entity. **Templater** - The templater is responsible for actually running the chosen templater on top of the previously returned temporary directory from the -**Preprarer**. We advise making these docker containers as it can keep all -dependencies, for example Cookiecutter, self contained and not a dependency on +**Preprarer**. We advise making these Docker containers as it can keep all +dependencies--for example Cookiecutter--self contained and not a dependency on the host machine. **Publisher** - The publisher is responsible for taking the finished directory, @@ -50,11 +50,11 @@ passed through to the scaffolder backend. ### How it works -The main of the heavy lifting is done in the +Most of the heavy lifting is done in the [router.ts](https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/src/service/router.ts#L93) file in the `scaffolder-backend` plugin. -There are 2 routes defined in the router. `POST /v1/jobs` and +There are two routes defined in the router: `POST /v1/jobs` and `GET /v1/job/:jobId` To create a scaffolding job, a JSON object containing the @@ -78,7 +78,7 @@ additional templating values must be posted as the post body. The values should represent something that is valid with the `schema` part of the [Template Entity](../../software-catalog/descriptor-format.md#kind-template) -Once that has been posted, a job will be setup with different stages. And the +Once that has been posted, a job will be setup with different stages, and the job processor will complete each stage before moving onto the next stage, whilst collecting logs and mutating the running job. diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 503fbb7a3d..1c67676b3e 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -4,8 +4,8 @@ title: Software Templates --- The Software Templates part of Backstage is a tool that can help you create -Components inside Backstage. It by default has the ability to load skeletons of -code, template in some variables and then publish the template to some location +Components inside Backstage. By default, it has the ability to load skeletons of +code, template in some variables, and then publish the template to some location like GitHub.