Merge branch 'master' into ndudnik/github-actions-annotation

This commit is contained in:
Nikita Dudnik
2020-07-28 09:35:15 +02:00
committed by GitHub
130 changed files with 5850 additions and 436 deletions
+17
View File
@@ -13,6 +13,18 @@ jobs:
build:
runs-on: ${{ matrix.os }}
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432/tcp
# needed because the postgres container does not provide a healthcheck
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
strategy:
matrix:
os: [ubuntu-latest]
@@ -44,6 +56,11 @@ jobs:
- run: yarn tsc
- run: yarn build
- name: verify app and plugin creation
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
run: |
sudo sysctl fs.inotify.max_user_watches=524288
node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
+13 -13
View File
@@ -1,13 +1,13 @@
| Organization | Contact | Description of Use |
| ---------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------- |
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D
| Organization | Contact | Description of Use |
| --------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
+69
View File
@@ -25,3 +25,72 @@ techdocs:
sentry:
organization: spotify
auth:
providers:
google:
development:
appOrigin: "http://localhost:3000/"
secure: false
clientId:
$secret:
env: AUTH_GOOGLE_CLIENT_ID
clientSecret:
$secret:
env: AUTH_GOOGLE_CLIENT_SECRET
github:
development:
appOrigin: "http://localhost:3000/"
secure: false
clientId:
$secret:
env: AUTH_GITHUB_CLIENT_ID
clientSecret:
$secret:
env: AUTH_GITHUB_CLIENT_SECRET
gitlab:
development:
appOrigin: "http://localhost:3000/"
secure: false
clientId:
$secret:
env: AUTH_GITLAB_CLIENT_ID
clientSecret:
$secret:
env: AUTH_GITLAB_CLIENT_SECRET
audience:
$secret:
env: GITLAB_BASE_URL
# saml:
# development:
# entryPoint: "http://localhost:7001/"
# issuer: "passport-saml"
okta:
development:
appOrigin: "http://localhost:3000/"
secure: false
clientId:
$secret:
env: AUTH_OKTA_CLIENT_ID
clientSecret:
$secret:
env: AUTH_OKTA_CLIENT_SECRET
audience:
$secret:
env: AUTH_OKTA_AUDIENCE
oauth2:
development:
appOrigin: "http://localhost:3000/"
secure: false
clientId:
$secret:
env: AUTH_OAUTH2_CLIENT_ID
clientSecret:
$secret:
env: AUTH_OAUTH2_CLIENT_SECRET
authorizationURL:
$secret:
env: AUTH_OAUTH2_AUTH_URL
tokenURL:
$secret:
env: AUTH_OAUTH2_TOKEN_URL
+3 -1
View File
@@ -77,7 +77,8 @@ better yet, a pull request.
- [Figma resources](dls/figma.md)
- API references
- TypeScript API
- [Utilities](api/utility-apis.md)
- [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)
@@ -93,6 +94,7 @@ better yet, a pull request.
- [ADR004 - Module Export Structure](architecture-decisions/adr004-module-export-structure.md)
- [ADR005 - Catalog Core Entities](architecture-decisions/adr005-catalog-core-entities.md)
- [ADR006 - Avoid React.FC and React.SFC](architecture-decisions/adr006-avoid-react-fc.md)
- [ADR007 - Use MSW for Mocking Network Requests](architecture-decisions/adr007-use-msw-to-mock-service-requests.md)
- [Contribute](../CONTRIBUTING.md)
- [Support](overview/support.md)
- [FAQ](FAQ.md)
+2 -1
View File
@@ -19,7 +19,8 @@ during their entire life cycle.
Each Utility API is tied to an `ApiRef` instance, which is a global singleton
object without any additional state or functionality, its only purpose is to
reference Utility APIs. `ApiRef`s are create using `createApiRef`, which is
exported by `@backstage/core`. There are many predefined Utility APIs defined in
exported by `@backstage/core`. There are many
[predefined Utility APIs](../reference/utility-apis/README.md) defined in
`@backstage/core`, and they're all exported with a name of the pattern
`*ApiRef`, for example `errorApiRef`.
@@ -0,0 +1,60 @@
# ADR007: Use MSW to mock http requests
## Context
Network request mocking can be a total pain sometimes, in all different types of
tests, unit tests to e2e tests always have their own implementation of mocking
these requests. There's been traction in the outer community towards using this
library to mock network requests by using an express style declaration for
routes. react-testing-library suggests using this library instead of mocking
fetch directly wether this be in a browser or in node.
https://github.com/mswjs/msw
## Decision
Moving forward, we have decided that any `fetch` or `XMLHTTPRequest` that
happens, should be mocked by using `msw`.
Here is an example:
```ts
import { setupWorker, rest } from 'msw';
const worker = setupWorker(
rest.get('*/user/:userId', (req, res, ctx) => {
return res(
ctx.json({
firstName: 'John',
lastName: 'Maverick',
}),
);
}),
);
// Start the Mock Service Worker
worker.start();
```
and in a more real life scenario, taken from
[CatalogClient.test.ts](https://github.com/spotify/backstage/blob/f3245c4f8f0b6b2625c4a6d5d50161b612fb4757/plugins/catalog/src/api/CatalogClient.test.ts)
```ts
beforeEach(() => {
server.use(
rest.get(`${mockApiOrigin}${mockBasePath}/entities`, (_, res, ctx) => {
return res(ctx.json(defaultResponse));
}),
);
});
it('should entities from correct endpoint', async () => {
const entities = await client.getEntities();
expect(entities).toEqual(defaultResponse);
});
```
## Consequences
- A little more code to write
- Gradually will replace the codebase with `msw`
+2 -1
View File
@@ -26,7 +26,8 @@ OAuth helps in that regard.
The method with which frontend plugins request access to third party services is
through [Utility APIs](../api/utility-apis.md) for each service provider. For a
full list of providers, see [TODO](#TODO).
full list of providers, see the
[Utility API References](../reference/utility-apis/README.md).
### Identity - WIP
+114
View File
@@ -0,0 +1,114 @@
# AlertApi
The AlertApi type is defined at
[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AlertApi.ts#L29).
The following Utility API implements this type: [alertApiRef](./README.md#alert)
## Members
### post()
Post an alert for handling by the application.
<pre>
post(alert: <a href="#alertmessage">AlertMessage</a>): void
</pre>
### alert\$()
Observe alerts posted by other parts of the application.
<pre>
alert$(): <a href="#observable">Observable</a>&lt;<a href="#alertmessage">AlertMessage</a>&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AlertMessage
<pre>
export type AlertMessage = {
message: string;
// Severity will default to success since that is what material ui defaults the value to.
severity?: 'success' | 'info' | 'warning' | 'error';
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AlertApi.ts#L19).
Referenced by: [post](#post), [alert\$](#alert).
### Observable
Observable sequence of values and errors, see TC39.
https://github.com/tc39/proposal-observable
This is used as a common return type for observable values and can be created
using many different observable implementations, such as zen-observable or
RxJS 5.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
Referenced by: [alert\$](#alert).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
+225
View File
@@ -0,0 +1,225 @@
# AppThemeApi
The AppThemeApi type is defined at
[packages/core-api/src/apis/definitions/AppThemeApi.ts:50](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AppThemeApi.ts#L50).
The following Utility API implements this type:
[appThemeApiRef](./README.md#apptheme)
## Members
### getInstalledThemes()
Get a list of available themes.
<pre>
getInstalledThemes(): <a href="#apptheme">AppTheme</a>[]
</pre>
### activeThemeId\$()
Observe the currently selected theme. A value of undefined means no specific
theme has been selected.
<pre>
activeThemeId$(): <a href="#observable">Observable</a>&lt;string | undefined&gt;
</pre>
### getActiveThemeId()
Get the current theme ID. Returns undefined if no specific theme is selected.
<pre>
getActiveThemeId(): string | undefined
</pre>
### setActiveThemeId()
Set a specific theme to use in the app, overriding the default theme selection.
Clear the selection by passing in undefined.
<pre>
setActiveThemeId(themeId?: string): void
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AppTheme
Describes a theme provided by the app.
<pre>
export type AppTheme = {
/**
* ID used to remember theme selections.
*/
id: string;
/**
* Title of the theme
*/
title: string;
/**
* Theme variant
*/
variant: 'light' | 'dark';
/**
* The specialized MaterialUI theme instance.
*/
theme: <a href="#backstagetheme">BackstageTheme</a>;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/AppThemeApi.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AppThemeApi.ts#L24).
Referenced by: [getInstalledThemes](#getinstalledthemes).
### BackstagePalette
<pre>
export type BackstagePalette = Palette &amp; <a href="#paletteadditions">PaletteAdditions</a>
</pre>
Defined at
[packages/theme/src/types.ts:63](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L63).
Referenced by: [BackstageTheme](#backstagetheme).
### BackstageTheme
<pre>
export interface BackstageTheme extends Theme {
palette: <a href="#backstagepalette">BackstagePalette</a>;
}
</pre>
Defined at
[packages/theme/src/types.ts:66](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L66).
Referenced by: [AppTheme](#apptheme).
### Observable
Observable sequence of values and errors, see TC39.
https://github.com/tc39/proposal-observable
This is used as a common return type for observable values and can be created
using many different observable implementations, such as zen-observable or
RxJS 5.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
Referenced by: [activeThemeId\$](#activethemeid).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
### PaletteAdditions
<pre>
type PaletteAdditions = {
status: {
ok: string;
warning: string;
error: string;
pending: string;
running: string;
aborted: string;
};
border: string;
textContrast: string;
textVerySubtle: string;
textSubtle: string;
highlight: string;
errorBackground: string;
warningBackground: string;
infoBackground: string;
errorText: string;
infoText: string;
warningText: string;
linkHover: string;
link: string;
gold: string;
sidebar: string;
tabbar: {
indicator: string;
};
bursts: {
fontColor: string;
slackChannelText: string;
backgroundColor: {
default: string;
};
};
pinSidebarButton: {
icon: string;
background: string;
};
}
</pre>
Defined at
[packages/theme/src/types.ts:23](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L23).
Referenced by: [BackstagePalette](#backstagepalette).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
@@ -0,0 +1,88 @@
# BackstageIdentityApi
The BackstageIdentityApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:144](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L144).
The following Utility APIs implement this type:
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [oktaAuthApiRef](./README.md#oktaauth)
## Members
### getBackstageIdentity()
Get the user's identity within Backstage. This should normally not be called
directly, use the @IdentityApi instead.
If the optional flag is not set, a session is guaranteed to be returned, while
if the optional flag is set, the session may be undefined. See
@AuthRequestOptions for more details.
<pre>
getBackstageIdentity(
options?: <a href="#authrequestoptions">AuthRequestOptions</a>,
): Promise&lt;<a href="#backstageidentity">BackstageIdentity</a> | undefined&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AuthRequestOptions
<pre>
export type AuthRequestOptions = {
/**
* If this is set to true, the user will not be prompted to log in,
* and an empty response will be returned if there is no existing session.
*
* This can be used to perform a check whether the user is logged in, or if you don't
* want to force a user to be logged in, but provide functionality if they already are.
*
* @default false
*/
optional?: boolean;
/**
* If this is set to true, the request will bypass the regular oauth login modal
* and open the login popup directly.
*
* The method must be called synchronously from a user action for this to work in all browsers.
*
* @default false
*/
instantPopup?: boolean;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getBackstageIdentity](#getbackstageidentity).
### BackstageIdentity
<pre>
export type BackstageIdentity = {
/**
* The backstage user ID.
*/
id: string;
/**
* An ID token that can be used to authenticate the user within Backstage.
*/
idToken: string;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:157](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L157).
Referenced by: [getBackstageIdentity](#getbackstageidentity).
+179
View File
@@ -0,0 +1,179 @@
# Config
The Config type is defined at
[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L32).
The following Utility API implements this type:
[configApiRef](./README.md#config)
## Members
### keys()
<pre>
keys(): string[]
</pre>
### get()
<pre>
get(key: string): <a href="#jsonvalue">JsonValue</a>
</pre>
### getOptional()
<pre>
getOptional(key: string): <a href="#jsonvalue">JsonValue</a> | undefined
</pre>
### getConfig()
<pre>
getConfig(key: string): <a href="#config">Config</a>
</pre>
### getOptionalConfig()
<pre>
getOptionalConfig(key: string): <a href="#config">Config</a> | undefined
</pre>
### getConfigArray()
<pre>
getConfigArray(key: string): <a href="#config">Config</a>[]
</pre>
### getOptionalConfigArray()
<pre>
getOptionalConfigArray(key: string): <a href="#config">Config</a>[] | undefined
</pre>
### getNumber()
<pre>
getNumber(key: string): number
</pre>
### getOptionalNumber()
<pre>
getOptionalNumber(key: string): number | undefined
</pre>
### getBoolean()
<pre>
getBoolean(key: string): boolean
</pre>
### getOptionalBoolean()
<pre>
getOptionalBoolean(key: string): boolean | undefined
</pre>
### getString()
<pre>
getString(key: string): string
</pre>
### getOptionalString()
<pre>
getOptionalString(key: string): string | undefined
</pre>
### getStringArray()
<pre>
getStringArray(key: string): string[]
</pre>
### getOptionalStringArray()
<pre>
getOptionalStringArray(key: string): string[] | undefined
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### Config
<pre>
export type Config = {
keys(): string[];
get(key: string): <a href="#jsonvalue">JsonValue</a>;
getOptional(key: string): <a href="#jsonvalue">JsonValue</a> | undefined;
getConfig(key: string): Config;
getOptionalConfig(key: string): <a href="#config">Config</a> | undefined;
getConfigArray(key: string): <a href="#config">Config</a>[];
getOptionalConfigArray(key: string): <a href="#config">Config</a>[] | undefined;
getNumber(key: string): number;
getOptionalNumber(key: string): number | undefined;
getBoolean(key: string): boolean;
getOptionalBoolean(key: string): boolean | undefined;
getString(key: string): string;
getOptionalString(key: string): string | undefined;
getStringArray(key: string): string[];
getOptionalStringArray(key: string): string[] | undefined;
}
</pre>
Defined at
[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L32).
Referenced by: [getConfig](#getconfig), [getOptionalConfig](#getoptionalconfig),
[getConfigArray](#getconfigarray),
[getOptionalConfigArray](#getoptionalconfigarray), [Config](#config).
### JsonArray
<pre>
export type JsonArray = <a href="#jsonvalue">JsonValue</a>[]
</pre>
Defined at
[packages/config/src/types.ts:18](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L18).
Referenced by: [JsonValue](#jsonvalue).
### JsonObject
<pre>
export type JsonObject = { [key in string]?: <a href="#jsonvalue">JsonValue</a> }
</pre>
Defined at
[packages/config/src/types.ts:17](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L17).
Referenced by: [JsonValue](#jsonvalue).
### JsonValue
<pre>
export type JsonValue =
| <a href="#jsonobject">JsonObject</a>
| <a href="#jsonarray">JsonArray</a>
| number
| string
| boolean
| null
</pre>
Defined at
[packages/config/src/types.ts:19](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L19).
Referenced by: [get](#get), [getOptional](#getoptional),
[JsonObject](#jsonobject), [JsonArray](#jsonarray), [Config](#config).
+134
View File
@@ -0,0 +1,134 @@
# ErrorApi
The ErrorApi type is defined at
[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L53).
The following Utility API implements this type: [errorApiRef](./README.md#error)
## Members
### post()
Post an error for handling by the application.
<pre>
post(error: <a href="#error">Error</a>, context?: <a href="#errorcontext">ErrorContext</a>): void
</pre>
### error\$()
Observe errors posted by other parts of the application.
<pre>
error$(): <a href="#observable">Observable</a>&lt;{ error: <a href="#error">Error</a>; context?: <a href="#errorcontext">ErrorContext</a> }&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### Error
Mirrors the javascript Error class, for the purpose of providing documentation
and optional fields.
<pre>
type Error = {
name: string;
message: string;
stack?: string;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L24).
Referenced by: [post](#post), [error\$](#error).
### ErrorContext
Provides additional information about an error that was posted to the
application.
<pre>
export type ErrorContext = {
// If set to true, this error should not be displayed to the user. Defaults to false.
hidden?: boolean;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L33).
Referenced by: [post](#post), [error\$](#error).
### Observable
Observable sequence of values and errors, see TC39.
https://github.com/tc39/proposal-observable
This is used as a common return type for observable values and can be created
using many different observable implementations, such as zen-observable or
RxJS 5.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
Referenced by: [error\$](#error).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
@@ -0,0 +1,33 @@
# FeatureFlagsApi
The FeatureFlagsApi type is defined at
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:41](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L41).
The following Utility API implements this type:
[featureFlagsApiRef](./README.md#featureflags)
## Members
### registeredFeatureFlags
Store a list of registered feature flags.
<pre>
registeredFeatureFlags: FeatureFlagsRegistryItem[]
</pre>
### getFlags()
Get a list of all feature flags from the current user.
<pre>
getFlags(): UserFlags
</pre>
### getRegisteredFlags()
Get a list of all registered flags.
<pre>
getRegisteredFlags(): FeatureFlagsRegistry
</pre>
@@ -0,0 +1,81 @@
# IdentityApi
The IdentityApi type is defined at
[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/IdentityApi.ts#L22).
The following Utility API implements this type:
[identityApiRef](./README.md#identity)
## Members
### getUserId()
The ID of the signed in user. This ID is not meant to be presented to the user,
but used as an opaque string to pass on to backends or use in frontend logic.
TODO: The intention of the user ID is to be able to tie the user to an identity
that is known by the catalog and/or identity backend. It should for example be
possible to fetch all owned components using this ID.
<pre>
getUserId(): string
</pre>
### getProfile()
The profile of the signed in user.
<pre>
getProfile(): <a href="#profileinfo">ProfileInfo</a>
</pre>
### getIdToken()
An OpenID Connect ID Token which proves the identity of the signed in user.
The ID token will be undefined if the signed in user does not have a verified
identity, such as a demo user or mocked user for e2e tests.
<pre>
getIdToken(): Promise&lt;string | undefined&gt;
</pre>
### logout()
Log out the current user
<pre>
logout(): Promise&lt;void&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### ProfileInfo
Profile information of the user.
<pre>
export type ProfileInfo = {
/**
* Email ID.
*/
email?: string;
/**
* Display name that can be presented to the user.
*/
displayName?: string;
/**
* URL to an avatar image of the user.
*/
picture?: string;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L172).
Referenced by: [getProfile](#getprofile).
+119
View File
@@ -0,0 +1,119 @@
# OAuthApi
The OAuthApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L67).
The following Utility APIs implement this type:
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
## Members
### getAccessToken()
Requests an OAuth 2 Access Token, optionally with a set of scopes. The access
token allows you to make requests on behalf of the user, and the copes may grant
you broader access, depending on the auth provider.
Each auth provider has separate handling of scope, so you need to look at the
documentation for each one to know what scope you need to request.
This method is cheap and should be called each time an access token is used. Do
not for example store the access token in React component state, as that could
cause the token to expire. Instead fetch a new access token for each request.
Be sure to include all required scopes when requesting an access token. When
testing your implementation it is best to log out the Backstage session and then
visit your plugin page directly, as you might already have some required scopes
in your existing session. Not requesting the correct scopes can lead to 403 or
other authorization errors, which can be tricky to debug.
If the user has not yet granted access to the provider and the set of requested
scopes, the user will be prompted to log in. The returned promise will not
resolve until the user has successfully logged in. The returned promise can be
rejected, but only if the user rejects the login request.
<pre>
getAccessToken(
scope?: <a href="#oauthscope">OAuthScope</a>,
options?: <a href="#authrequestoptions">AuthRequestOptions</a>,
): Promise&lt;string&gt;
</pre>
### logout()
Log out the user's session. This will reload the page.
<pre>
logout(): Promise&lt;void&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AuthRequestOptions
<pre>
export type AuthRequestOptions = {
/**
* If this is set to true, the user will not be prompted to log in,
* and an empty response will be returned if there is no existing session.
*
* This can be used to perform a check whether the user is logged in, or if you don't
* want to force a user to be logged in, but provide functionality if they already are.
*
* @default false
*/
optional?: boolean;
/**
* If this is set to true, the request will bypass the regular oauth login modal
* and open the login popup directly.
*
* The method must be called synchronously from a user action for this to work in all browsers.
*
* @default false
*/
instantPopup?: boolean;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getAccessToken](#getaccesstoken).
### OAuthScope
This file contains declarations for common interfaces of auth-related APIs. The
declarations should be used to signal which type of authentication and
authorization methods each separate auth provider supports.
For example, a Google OAuth provider that supports OAuth 2 and OpenID Connect,
would be declared as follows:
const googleAuthApiRef = createApiRef<OAuthApi & OpenIDConnectApi>({ ... })
An array of scopes, or a scope string formatted according to the auth provider,
which is typically a space separated list.
See the documentation for each auth provider for the list of scopes supported by
each provider.
<pre>
export type OAuthScope = string | string[]
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L38).
Referenced by: [getAccessToken](#getaccesstoken).
@@ -0,0 +1,232 @@
# OAuthRequestApi
The OAuthRequestApi type is defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99).
The following Utility API implements this type:
[oauthRequestApiRef](./README.md#oauthrequest)
## Members
### createAuthRequester()
A utility for showing login popups or similar things, and merging together
multiple requests for different scopes into one request that inclues all scopes.
The passed in options provide information about the login provider, and how to
handle auth requests.
The returned AuthRequester function is used to request login with new scopes.
These requests are merged together and forwarded to the auth handler, as soon as
a consumer of auth requests triggers an auth flow.
See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info.
<pre>
createAuthRequester&lt;AuthResponse&gt;(
options: <a href="#authrequesteroptions">AuthRequesterOptions</a>&lt;AuthResponse&gt;,
): <a href="#authrequester">AuthRequester</a>&lt;AuthResponse&gt;
</pre>
### authRequest\$()
Observers panding auth requests. The returned observable will emit all current
active auth request, at most one for each created auth requester.
Each request has its own info about the login provider, forwarded from the auth
requester options.
Depending on user interaction, the request should either be rejected, or used to
trigger the auth handler. If the request is rejected, all pending AuthRequester
calls will fail with a "RejectedError". If a auth is triggered, and the auth
handler resolves successfully, then all currently pending AuthRequester calls
will resolve to the value returned by the onAuthRequest call.
<pre>
authRequest$(): <a href="#observable">Observable</a>&lt;<a href="#pendingauthrequest">PendingAuthRequest</a>[]&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AuthProvider
Information about the auth provider that we're requesting a login towards.
This should be shown to the user so that they can be informed about what login
is being requested before a popup is shown.
<pre>
export type AuthProvider = {
/**
* Title for the auth provider, for example "GitHub"
*/
title: string;
/**
* Icon for the auth provider.
*/
icon: IconComponent;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27).
Referenced by: [AuthRequesterOptions](#authrequesteroptions),
[PendingAuthRequest](#pendingauthrequest).
### AuthRequester
Function used to trigger new auth requests for a set of scopes.
The returned promise will resolve to the same value returned by the
onAuthRequest in the AuthRequesterOptions. Or rejected, if the request is
rejected.
This function can be called multiple times before the promise resolves. All
calls will be merged into one request, and the scopes forwarded to the
onAuthRequest will be the union of all requested scopes.
<pre>
export type AuthRequester&lt;AuthResponse&gt; = (
scopes: Set&lt;string&gt;,
) =&gt; Promise&lt;AuthResponse&gt;
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66).
Referenced by: [createAuthRequester](#createauthrequester).
### AuthRequesterOptions
Describes how to handle auth requests. Both how to show them to the user, and
what to do when the user accesses the auth request.
<pre>
export type AuthRequesterOptions&lt;AuthResponse&gt; = {
/**
* Information about the auth provider, which will be forwarded to auth requests.
*/
provider: <a href="#authprovider">AuthProvider</a>;
/**
* Implementation of the auth flow, which will be called synchronously when
* trigger() is called on an auth requests.
*/
onAuthRequest(scopes: Set&lt;string&gt;): Promise&lt;AuthResponse&gt;;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43).
Referenced by: [createAuthRequester](#createauthrequester).
### Observable
Observable sequence of values and errors, see TC39.
https://github.com/tc39/proposal-observable
This is used as a common return type for observable values and can be created
using many different observable implementations, such as zen-observable or
RxJS 5.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
Referenced by: [authRequest\$](#authrequest).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
### PendingAuthRequest
An pending auth request for a single auth provider. The request will remain in
this pending state until either reject() or trigger() is called.
Any new requests for the same provider are merged into the existing pending
request, meaning there will only ever be a single pending request for a given
provider.
<pre>
export type PendingAuthRequest = {
/**
* Information about the auth provider, as given in the AuthRequesterOptions
*/
provider: <a href="#authprovider">AuthProvider</a>;
/**
* Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError".
*/
reject: () =&gt; void;
/**
* Trigger the auth request to continue the auth flow, by for example showing a popup.
*
* Synchronously calls onAuthRequest with all scope currently in the request.
*/
trigger(): Promise&lt;void&gt;;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77).
Referenced by: [authRequest\$](#authrequest).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
@@ -0,0 +1,75 @@
# OpenIdConnectApi
The OpenIdConnectApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:104](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L104).
The following Utility APIs implement this type:
- [googleAuthApiRef](./README.md#googleauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
## Members
### getIdToken()
Requests an OpenID Connect ID Token.
This method is cheap and should be called each time an ID token is used. Do not
for example store the id token in React component state, as that could cause the
token to expire. Instead fetch a new id token for each request.
If the user has not yet logged in to Google inside Backstage, the user will be
prompted to log in. The returned promise will not resolve until the user has
successfully logged in. The returned promise can be rejected, but only if the
user rejects the login request.
<pre>
getIdToken(options?: <a href="#authrequestoptions">AuthRequestOptions</a>): Promise&lt;string&gt;
</pre>
### logout()
Log out the user's session. This will reload the page.
<pre>
logout(): Promise&lt;void&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AuthRequestOptions
<pre>
export type AuthRequestOptions = {
/**
* If this is set to true, the user will not be prompted to log in,
* and an empty response will be returned if there is no existing session.
*
* This can be used to perform a check whether the user is logged in, or if you don't
* want to force a user to be logged in, but provide functionality if they already are.
*
* @default false
*/
optional?: boolean;
/**
* If this is set to true, the request will bypass the regular oauth login modal
* and open the login popup directly.
*
* The method must be called synchronously from a user action for this to work in all browsers.
*
* @default false
*/
instantPopup?: boolean;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getIdToken](#getidtoken).
@@ -0,0 +1,94 @@
# ProfileInfoApi
The ProfileInfoApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:127](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L127).
The following Utility APIs implement this type:
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
## Members
### getProfile()
Get profile information for the user as supplied by this auth provider.
If the optional flag is not set, a session is guaranteed to be returned, while
if the optional flag is set, the session may be undefined. See
@AuthRequestOptions for more details.
<pre>
getProfile(options?: <a href="#authrequestoptions">AuthRequestOptions</a>): Promise&lt;<a href="#profileinfo">ProfileInfo</a> | undefined&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AuthRequestOptions
<pre>
export type AuthRequestOptions = {
/**
* If this is set to true, the user will not be prompted to log in,
* and an empty response will be returned if there is no existing session.
*
* This can be used to perform a check whether the user is logged in, or if you don't
* want to force a user to be logged in, but provide functionality if they already are.
*
* @default false
*/
optional?: boolean;
/**
* If this is set to true, the request will bypass the regular oauth login modal
* and open the login popup directly.
*
* The method must be called synchronously from a user action for this to work in all browsers.
*
* @default false
*/
instantPopup?: boolean;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getProfile](#getprofile).
### ProfileInfo
Profile information of the user.
<pre>
export type ProfileInfo = {
/**
* Email ID.
*/
email?: string;
/**
* Display name that can be presented to the user.
*/
displayName?: string;
/**
* URL to an avatar image of the user.
*/
picture?: string;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L172).
Referenced by: [getProfile](#getprofile).
+139
View File
@@ -0,0 +1,139 @@
# Backstage Core Utility APIs
The following is a list of all Utility APIs defined by `@backstage/core`. They
are available to use by plugins and components, and can be accessed using the
`useApi` hook, also provided by `@backstage/core`. For more information, see
https://github.com/spotify/backstage/blob/master/docs/api/utility-apis.md.
### alert
Used to report alerts and forward them to the app
Implemented type: [AlertApi](./AlertApi.md)
ApiRef:
[alertApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AlertApi.ts#L41)
### appTheme
API Used to configure the app theme, and enumerate options
Implemented type: [AppThemeApi](./AppThemeApi.md)
ApiRef:
[appThemeApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AppThemeApi.ts#L74)
### config
Used to access runtime configuration
Implemented type: [Config](./Config.md)
ApiRef:
[configApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ConfigApi.ts#L22)
### error
Used to report errors and forward them to the app
Implemented type: [ErrorApi](./ErrorApi.md)
ApiRef:
[errorApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L65)
### featureFlags
Used to toggle functionality in features across Backstage
Implemented type: [FeatureFlagsApi](./FeatureFlagsApi.md)
ApiRef:
[featureFlagsApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L58)
### githubAuth
Provides authentication towards Github APIs
Implemented types: [OAuthApi](./OAuthApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[githubAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L230)
### gitlabAuth
Provides authentication towards Gitlab APIs
Implemented types: [OAuthApi](./OAuthApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[gitlabAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L260)
### googleAuth
Provides authentication towards Google APIs and identities
Implemented types: [OAuthApi](./OAuthApi.md),
[OpenIdConnectApi](./OpenIdConnectApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[googleAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L213)
### identity
Provides access to the identity of the signed in user
Implemented type: [IdentityApi](./IdentityApi.md)
ApiRef:
[identityApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/IdentityApi.ts#L54)
### oauth2
Example of how to use oauth2 custom provider
Implemented types: [OAuthApi](./OAuthApi.md),
[OpenIdConnectApi](./OpenIdConnectApi.md),
[ProfileInfoApi](./ProfileInfoApi.md), [SessionStateApi](./SessionStateApi.md)
ApiRef:
[oauth2ApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L270)
### oauthRequest
An API for implementing unified OAuth flows in Backstage
Implemented type: [OAuthRequestApi](./OAuthRequestApi.md)
ApiRef:
[oauthRequestApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130)
### oktaAuth
Provides authentication towards Okta APIs
Implemented types: [OAuthApi](./OAuthApi.md),
[OpenIdConnectApi](./OpenIdConnectApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[oktaAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L243)
### storage
Provides the ability to store data which is unique to the user
Implemented type: [StorageApi](./StorageApi.md)
ApiRef:
[storageApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L68)
@@ -0,0 +1,115 @@
# SessionStateApi
The SessionStateApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:201](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L201).
The following Utility APIs implement this type:
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
## Members
### sessionState\$()
<pre>
sessionState$(): <a href="#observable">Observable</a>&lt;<a href="#sessionstate">SessionState</a>&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### Observable
Observable sequence of values and errors, see TC39.
https://github.com/tc39/proposal-observable
This is used as a common return type for observable values and can be created
using many different observable implementations, such as zen-observable or
RxJS 5.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
Referenced by: [sessionState\$](#sessionstate).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
### SessionState
Session state values passed to subscribers of the SessionStateApi.
<pre>
export enum SessionState {
SignedIn = 'SignedIn',
SignedOut = 'SignedOut',
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:192](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L192).
Referenced by: [sessionState\$](#sessionstate).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
+186
View File
@@ -0,0 +1,186 @@
# StorageApi
The StorageApi type is defined at
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
The following Utility API implements this type:
[storageApiRef](./README.md#storage)
## Members
### forBucket()
Create a bucket to store data in.
<pre>
forBucket(name: string): <a href="#storageapi">StorageApi</a>
</pre>
### get()
Get the current value for persistent data, use observe\$ to be notified of
updates.
<pre>
get&lt;T&gt;(key: string): T | undefined
</pre>
### remove()
Remove persistent data.
<pre>
remove(key: string): Promise&lt;void&gt;
</pre>
### set()
Save persistant data, and emit messages to anyone that is using observe\$ for
this key
<pre>
set(key: string, data: any): Promise&lt;void&gt;
</pre>
### observe\$()
Observe changes on a particular key in the bucket
<pre>
observe$&lt;T&gt;(key: string): <a href="#observable">Observable</a>&lt;<a href="#storagevaluechange">StorageValueChange</a>&lt;T&gt;&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### Observable
Observable sequence of values and errors, see TC39.
https://github.com/tc39/proposal-observable
This is used as a common return type for observable values and can be created
using many different observable implementations, such as zen-observable or
RxJS 5.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
Referenced by: [observe\$](#observe), [StorageApi](#storageapi).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
### StorageApi
<pre>
export interface StorageApi {
/**
* Create a bucket to store data in.
* @param {String} name Namespace for the storage to be stored under,
* will inherit previous namespaces too
*/
forBucket(name: string): StorageApi;
/**
* Get the current value for persistent data, use observe$ to be notified of updates.
*
* @param {String} key Unique key associated with the data.
* @return {Object} data The data that should is stored.
*/
get&lt;T&gt;(key: string): T | undefined;
/**
* Remove persistent data.
*
* @param {String} key Unique key associated with the data.
*/
remove(key: string): Promise&lt;void&gt;;
/**
* Save persistant data, and emit messages to anyone that is using observe$ for this key
*
* @param {String} key Unique key associated with the data.
*/
set(key: string, data: any): Promise&lt;void&gt;;
/**
* Observe changes on a particular key in the bucket
* @param {String} key Unique key associated with the data
*/
observe$&lt;T&gt;(key: string): <a href="#observable">Observable</a>&lt;<a href="#storagevaluechange">StorageValueChange</a>&lt;T&gt;&gt;;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
Referenced by: [forBucket](#forbucket).
### StorageValueChange
<pre>
export type StorageValueChange&lt;T = any&gt; = {
key: string;
newValue?: T;
}
</pre>
Defined at
[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L21).
Referenced by: [observe\$](#observe), [StorageApi](#storageapi).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
+1
View File
@@ -91,6 +91,7 @@ nav:
- ADR004 - Module Export Structure: 'architecture-decisions/adr004-module-export-structure.md'
- ADR005 - Catalog Core Entities: 'architecture-decisions/adr005-catalog-core-entities.md'
- ADR006 - Avoid React.FC and React.SFC: 'architecture-decisions/adr006-avoid-react-fc.md'
- ADR007 - Use MSW for Network Request Mocking: 'architecture-decisions/adr007-use-msw-to-mock-service-requests.md'
- Contribute: '../CONTRIBUTING.md'
- Support: 'overview/support.md'
- FAQ: FAQ.md
+1
View File
@@ -15,6 +15,7 @@
"lint": "lerna run lint --since origin/master --",
"lint:all": "lerna run lint --",
"lint:type-deps": "node scripts/check-type-dependencies.js",
"docgen": "lerna run docgen",
"docker-build": "yarn workspace example-app build && docker build . -t spotify/backstage",
"docker-build:all": "yarn tsc && yarn build && yarn docker-build && yarn workspace example-backend build-image",
"create-plugin": "backstage-cli create-plugin",
+1 -1
View File
@@ -39,7 +39,7 @@
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/jquery": "^3.3.34",
"@types/node": "^12.0.0",
"@types/react-dom": "^16.9.8",
+9 -6
View File
@@ -19,6 +19,8 @@ import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
useApi,
configApiRef,
} from '@backstage/core';
import React, { FC } from 'react';
import Root from './components/Root';
@@ -30,12 +32,13 @@ const app = createApp({
apis,
plugins: Object.values(plugins),
components: {
SignInPage: props => (
<SignInPage
{...props}
providers={['guest', 'google', 'custom', 'okta', 'gitlab', 'github']}
/>
),
SignInPage: props => {
const configApi = useApi(configApiRef);
const providersConfig = configApi.getOptionalConfig('auth.providers');
const providers = providersConfig?.keys() ?? [];
return <SignInPage {...props} providers={['guest', ...providers]} />;
},
},
});
+1
View File
@@ -35,6 +35,7 @@
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"helmet": "^3.22.0",
"morgan": "^1.10.0",
"stoppable": "^1.1.0",
@@ -28,9 +28,9 @@ export type ErrorHandlerOptions = {
showStackTraces?: boolean;
/**
* Logger
* Logger instance to log any 5xx errors.
*
* If not specified, by default shows stack traces only in development mode.
* If not specified, the root logger will be used.
*/
logger?: Logger;
};
@@ -17,3 +17,4 @@
export * from './errorHandler';
export * from './notFoundHandler';
export * from './requestLoggingHandler';
export * from './statusCheckHandler';
@@ -0,0 +1,55 @@
/*
* 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 express from 'express';
import request from 'supertest';
import { statusCheckHandler } from './statusCheckHandler';
describe('statusCheckHandler', () => {
it('gives status 200 when using default', async () => {
const app = express();
app.use('/healthcheck', await statusCheckHandler());
const response = await request(app).get('/healthcheck');
expect(response.status).toBe(200);
expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
});
it('gives status 200 when status function returns true', async () => {
const app = express();
const status = { foo: 'bar' };
const statusCheck = () => Promise.resolve(status);
app.use('/healthcheck', await statusCheckHandler({ statusCheck }));
const response = await request(app).get('/healthcheck');
expect(response.status).toBe(200);
expect(response.text).toBe(JSON.stringify(status));
});
it('gives status 500 when status check throws an error', async () => {
const app = express();
const statusCheck = () => {
throw Error('error!');
};
app.use('/healthcheck', await statusCheckHandler({ statusCheck }));
const response = await request(app).get('/healthcheck');
expect(response.status).toBe(500);
});
});
@@ -0,0 +1,51 @@
/*
* 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 { NextFunction, Request, Response, RequestHandler } from 'express';
export type StatusCheck = () => Promise<any>;
export interface StatusCheckHandlerOptions {
/**
* Optional status function which returns a message.
*/
statusCheck?: StatusCheck;
}
/**
* Express middleware for status checks.
*
* This is commonly used to implement healthcheck and readiness routes.
*
* @param options An optional configuration object.
* @returns An Express error request handler
*/
export async function statusCheckHandler(
options: StatusCheckHandlerOptions = {},
): Promise<RequestHandler> {
const statusCheck: StatusCheck = options.statusCheck
? options.statusCheck
: () => Promise.resolve({ status: 'ok' });
return async (_request: Request, response: Response, next: NextFunction) => {
try {
const status = await statusCheck();
response.status(200).header('').send(status);
} catch (err) {
next(err);
}
};
}
@@ -0,0 +1,56 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import * as winston from 'winston';
import request from 'supertest';
import { createStatusCheckRouter } from './createStatusCheckRouter';
describe('createStatusCheckRouter', () => {
const logger = winston.createLogger();
it('gives status 200 when using default path', async () => {
const app = express();
app.use('', await createStatusCheckRouter({ logger }));
const response = await request(app).get('/healthcheck');
expect(response.status).toBe(200);
expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
});
it('gives status 200 when using custom path', async () => {
const app = express();
app.use('', await createStatusCheckRouter({ logger, path: '/ready' }));
const response = await request(app).get('/ready');
expect(response.status).toBe(200);
expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
});
it('gives status 500 when status check throws an error', async () => {
const app = express();
const statusCheck = () => {
throw Error('error!');
};
app.use('', await createStatusCheckRouter({ logger, statusCheck }));
const response = await request(app).get('/healthcheck');
expect(response.status).toBe(500);
});
});
@@ -0,0 +1,38 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Logger } from 'winston';
import Router from 'express-promise-router';
import express from 'express';
import { errorHandler, statusCheckHandler, StatusCheck } from '../middleware';
export interface StatusCheckRouterOptions {
logger: Logger;
path?: string;
statusCheck?: StatusCheck;
}
export async function createStatusCheckRouter(
options: StatusCheckRouterOptions,
): Promise<express.Router> {
const router = Router();
const { path = '/healthcheck', statusCheck } = options;
router.use(path, await statusCheckHandler({ statusCheck }));
router.use(errorHandler());
return router;
}
@@ -15,4 +15,5 @@
*/
export { createServiceBuilder } from './createServiceBuilder';
export { createStatusCheckRouter } from './createStatusCheckRouter';
export type { ServiceBuilder } from './types';
+2 -2
View File
@@ -16,7 +16,7 @@
import { ConfigReader } from '@backstage/config';
import cors from 'cors';
import { Router } from 'express';
import { Router, RequestHandler } from 'express';
import { Server } from 'http';
import { Logger } from 'winston';
@@ -64,7 +64,7 @@ export type ServiceBuilder = {
* @param root The root URL to bind to (e.g. "/api/function1")
* @param router An express router
*/
addRouter(root: string, router: Router): ServiceBuilder;
addRouter(root: string, router: Router | RequestHandler): ServiceBuilder;
/**
* Starts the server using the given settings.
+1
View File
@@ -35,6 +35,7 @@
"dockerode": "^3.2.0",
"express": "^4.17.1",
"knex": "^0.21.1",
"pg": "^8.3.0",
"sqlite3": "^4.2.0",
"winston": "^3.2.1"
},
+35 -7
View File
@@ -29,7 +29,8 @@ import {
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import knex from 'knex';
import knex, { PgConnectionConfig } from 'knex';
import healthcheck from './plugins/healthcheck';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
import identity from './plugins/identity';
@@ -46,11 +47,36 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
return (plugin: string): PluginEnvironment => {
const logger = getRootLogger().child({ type: 'plugin', plugin });
const database = knex({
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
});
// Supported DBs are sqlite and postgres
const isPg = [
'POSTGRES_USER',
'POSTGRES_HOST',
'POSTGRES_PASSWORD',
].every(key => config.getOptional(`backend.${key}`));
let knexConfig;
if (isPg) {
knexConfig = {
client: 'pg',
useNullAsDefault: true,
connection: {
port: config.getOptionalNumber('backend.POSTGRES_PORT'),
host: config.getString('backend.POSTGRES_HOST'),
user: config.getString('backend.POSTGRES_USER'),
password: config.getString('backend.POSTGRES_PASSWORD'),
database: `backstage_plugin_${plugin}`,
} as PgConnectionConfig,
};
} else {
knexConfig = {
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
};
}
const database = knex(knexConfig);
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
resource.run('PRAGMA foreign_keys = ON', () => {});
});
@@ -59,10 +85,11 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
}
async function main() {
const configs = await loadConfig();
const configs = await loadConfig({ shouldReadSecrets: true });
const configReader = ConfigReader.fromConfigs(configs);
const createEnv = makeCreateEnv(configs);
const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
@@ -75,6 +102,7 @@ async function main() {
const service = createServiceBuilder(module)
.loadConfig(configReader)
.addRouter('', await healthcheck(healthcheckEnv))
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter('/rollbar', await rollbar(rollbarEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
@@ -0,0 +1,22 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createStatusCheckRouter } from '@backstage/backend-common';
import { PluginEnvironment } from '../types';
export default async function createRouter({ logger }: PluginEnvironment) {
return await createStatusCheckRouter({ logger, path: '/healthcheck' });
}
+1 -1
View File
@@ -31,7 +31,7 @@
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.16",
"@types/express": "^4.17.6",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
"yaml": "^1.9.2"
},
+99 -3
View File
@@ -16,6 +16,7 @@
const os = require('os');
const fs = require('fs-extra');
const fetch = require('node-fetch');
const killTree = require('tree-kill');
const { resolve: resolvePath, join: joinPath } = require('path');
const Browser = require('zombie');
@@ -28,6 +29,7 @@ const {
waitForExit,
print,
} = require('./helpers');
const pgtools = require('pgtools');
async function main() {
const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-'));
@@ -36,8 +38,9 @@ async function main() {
print('Building dist workspace');
const workspaceDir = await buildDistWorkspace('workspace', rootDir);
const isPostgres = Boolean(process.env.POSTGRES_USER);
print('Creating a Backstage App');
const appDir = await createApp('test-app', workspaceDir, rootDir);
const appDir = await createApp('test-app', isPostgres, workspaceDir, rootDir);
print('Creating a Backstage Plugin');
const pluginName = await createPlugin('test-plugin', appDir);
@@ -45,6 +48,9 @@ async function main() {
print('Starting the app');
await testAppServe(pluginName, appDir);
print('Testing the backend startup');
await testBackendStart(appDir, isPostgres);
print('All tests successful, removing test dir');
await fs.remove(rootDir);
}
@@ -97,7 +103,7 @@ async function pinYarnVersion(dir) {
/**
* Creates a new app inside rootDir called test-app, using packages from the workspaceDir
*/
async function createApp(appName, workspaceDir, rootDir) {
async function createApp(appName, isPostgres, workspaceDir, rootDir) {
const child = spawnPiped(
[
'node',
@@ -119,6 +125,14 @@ async function createApp(appName, workspaceDir, rootDir) {
await waitFor(() => stdout.includes('Enter a name for the app'));
child.stdin.write(`${appName}\n`);
await waitFor(() => stdout.includes('Select database for the backend'));
if (!isPostgres) {
// Simulate down arrow press
child.stdin.write(`\u001B\u005B\u0042`);
}
child.stdin.write(`\n`);
print('Waiting for app create script to be done');
await waitForExit(child);
@@ -201,7 +215,7 @@ async function createPlugin(pluginName, appDir) {
await waitForExit(child);
const pluginDir = resolvePath(appDir, 'plugins', pluginName);
for (const cmd of [['lint'], ['test', '--no-watch']]) {
for (const cmd of [['tsc'], ['lint'], ['test', '--no-watch']]) {
print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`);
await runPlain(['yarn', ...cmd], { cwd: pluginDir });
}
@@ -250,5 +264,87 @@ async function testAppServe(pluginName, appDir) {
}
}
/** Creates PG databases (drops if exists before) */
async function createDB(database) {
const config = {
host: process.env.POSTGRES_HOST,
port: process.env.POSTGRES_PORT,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
};
try {
await pgtools.dropdb({ config }, database);
} catch (_) {
/* do nothing*/
}
return pgtools.createdb(config, database);
}
/**
* Start serving the newly created backend and make sure that all db migrations works correctly
*/
async function testBackendStart(appDir, isPostgres) {
if (isPostgres) {
print('Creating DBs');
await Promise.all(
[
'catalog',
'scaffolder',
'auth',
'identity',
'proxy',
'techdocs',
].map(name => createDB(`backstage_plugin_${name}`)),
);
print('Created DBs');
}
const child = spawnPiped(['yarn', 'workspace', 'backend', 'start'], {
cwd: appDir,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', data => {
stdout = stdout + data.toString('utf8');
});
child.stderr.on('data', data => {
stderr = stderr + data.toString('utf8');
});
let successful = false;
try {
await waitFor(() => stdout.includes('Listening on ') || stderr !== '');
if (stderr !== '') {
// Skipping the whole block
throw new Error(stderr);
}
print('Try to fetch entities from the backend');
// Try fetch entities, should be ok
await fetch('http://localhost:7000/catalog/entities').then(res =>
res.json(),
);
print('Entities fetched successfully');
successful = true;
} catch (error) {
throw new Error(`Backend failed to startup: ${error}`);
} finally {
print('Stopping the child process');
// Kill entire process group, otherwise we'll end up with hanging serve processes
killTree(child.pid);
}
try {
await waitForExit(child);
} catch (error) {
if (!successful) {
throw new Error(`Backend failed to startup: ${stderr}`);
}
print('Backend startup test finished successfully');
}
}
process.on('unhandledRejection', handleError);
main(process.argv.slice(2)).catch(handleError);
+6
View File
@@ -76,6 +76,12 @@ function handleError(err) {
}
}
/**
* Waits for fn() to be true
* Checks every 100ms
* .cancel() is available
* @returns {Promise} Promise of resolution
*/
function waitFor(fn) {
return new Promise(resolve => {
const handle = setInterval(() => {
+2
View File
@@ -68,7 +68,9 @@
"jest-css-modules": "^2.1.0",
"jest-esm-transformer": "^1.0.0",
"mini-css-extract-plugin": "^0.9.0",
"node-fetch": "^2.6.0",
"ora": "^4.0.3",
"pgtools": "^0.3.0",
"raw-loader": "^4.0.1",
"react": "^16.0.0",
"react-dev-utils": "^10.2.1",
@@ -104,8 +104,17 @@ export default async (cmd: Command): Promise<void> => {
return true;
},
},
{
type: 'list',
name: 'dbType',
message: chalk.blue('Select database for the backend [required]'),
// @ts-ignore
choices: ['PostgreSQL', 'SQLite'],
},
];
const answers: Answers = await inquirer.prompt(questions);
answers.dbTypePG = answers.dbType === 'PostgreSQL';
answers.dbTypeSqlite = answers.dbType === 'SQLite';
const templateDir = paths.resolveOwn('templates/default-app');
const tempDir = resolvePath(os.tmpdir(), answers.name);
+8 -4
View File
@@ -134,11 +134,15 @@ async function findTargetPackages(pkgNames: string[]): Promise<LernaPackage[]> {
throw new Error(`Package '${name}' not found`);
}
const pkgDeps = Object.keys(node.pkg.dependencies);
const localDeps: string[] = Array.from(node.localDependencies.keys());
const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep));
// Don't include dependencies of packages that are marked as bundled
if (!node.pkg.get('bundled')) {
const pkgDeps = Object.keys(node.pkg.dependencies);
const localDeps: string[] = Array.from(node.localDependencies.keys());
const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep));
searchNames.push(...filteredDeps);
}
searchNames.push(...filteredDeps);
targets.set(name, node.pkg);
}
+1 -1
View File
@@ -92,7 +92,7 @@ export async function runCheck(cmd: string, ...args: string[]) {
}
export async function waitForExit(
child: ChildProcess & { exitCode?: number },
child: ChildProcess & { exitCode: number | null },
name?: string,
): Promise<void> {
if (typeof child.exitCode === 'number') {
@@ -1,3 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
root: true,
};
@@ -4,3 +4,19 @@ app:
organization:
name: Acme Corporation
backend:
baseUrl: http://localhost:7000
listen: 0.0.0.0:7000
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
proxy:
'/test':
target: 'https://example.com'
changeOrigin: true
techdocs:
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
@@ -22,7 +22,7 @@
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/react-dom": "^16.9.8",
"cross-env": "^7.0.0",
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
@@ -0,0 +1,9 @@
FROM node:12
WORKDIR /usr/src/app
COPY . .
RUN yarn install --frozen-lockfile --production
CMD ["node", "packages/backend"]
@@ -0,0 +1,68 @@
# example-backend
This package is an EXAMPLE of a Backstage backend.
The main purpose of this package is to provide a test bed for Backstage plugins
that have a backend part. Feel free to experiment locally or within your fork
by adding dependencies and routes to this backend, to try things out.
Our goal is to eventually amend the create-app flow of the CLI, such that a
production ready version of a backend skeleton is made alongside the frontend
app. Until then, feel free to experiment here!
## Development
To run the example backend, first go to the project root and run
```bash
yarn install
yarn tsc
yarn build
```
You should only need to do this once.
After that, go to the `packages/backend` directory and run
```bash
AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \
AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \
AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \
AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \
LOG_LEVEL=debug \
yarn start
```
Substitute `x` for actual values, or leave them as
dummy values just to try out the backend without using the auth or sentry features.
The backend starts up on port 7000 per default.
## Populating The Catalog
If you want to use the catalog functionality, you need to add so called locations
to the backend. These are places where the backend can find some entity descriptor
data to consume and serve.
To get started, you can issue the following after starting the backend, from inside
the `plugins/catalog-backend` directory:
```bash
yarn mock-data
```
You should then start seeing data on `localhost:7000/catalog/entities`.
The catalog currently runs in-memory only, so feel free to try it out, but it will
need to be re-populated on next startup.
## Authentication
We chose [Passport](http://www.passportjs.org/) as authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/).
Read more about the [auth-backend](https://github.com/spotify/backstage/blob/master/plugins/auth-backend/README.md) and [how to add a new provider](https://github.com/spotify/backstage/blob/master/docs/auth/add-auth-provider.md)
## Documentation
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
@@ -0,0 +1,51 @@
{
"name": "backend",
"version": "0.0.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
"engines": {
"node": "12"
},
"scripts": {
"build": "backstage-cli backend:build",
"build-image": "backstage-cli backend:build-image example-backend",
"start": "backstage-cli backend:dev",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"clean": "backstage-cli clean",
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^{{version}}",
"@backstage/catalog-model": "^{{version}}",
"@backstage/config": "^0.1.1-alpha.13",
"@backstage/config-loader": "^0.1.1-alpha.13",
"@backstage/plugin-auth-backend": "^{{version}}",
"@backstage/plugin-catalog-backend": "^{{version}}",
"@backstage/plugin-identity-backend": "^{{version}}",
"@backstage/plugin-proxy-backend": "^{{version}}",
"@backstage/plugin-rollbar-backend": "^{{version}}",
"@backstage/plugin-scaffolder-backend": "^{{version}}",
"@backstage/plugin-sentry-backend": "^{{version}}",
"@backstage/plugin-techdocs-backend": "^{{version}}",
"@octokit/rest": "^18.0.0",
"dockerode": "^3.2.0",
"express": "^4.17.1",
"knex": "^0.21.1",
{{#if dbTypePG}}
"pg": "^8.3.0",
{{/if}}
{{#if dbTypeSqlite}}
"sqlite3": "^4.2.0",
{{/if}}
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.15",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/helmet": "^0.0.47"
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PluginEnvironment } from './types';
describe('test', () => {
it('unbreaks the test runner', () => {
const unbreaker = {} as PluginEnvironment;
expect(unbreaker).toBeTruthy();
});
});
@@ -0,0 +1,111 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Hi!
*
* Note that this is an EXAMPLE Backstage backend. Please check the README.
*
* Happy hacking!
*/
import {
createServiceBuilder,
getRootLogger,
useHotMemoize,
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
{{#if dbTypePG}}
import knex, { PgConnectionConfig } from 'knex';
{{/if}}
{{#if dbTypeSqlite}}
import knex from 'knex';
{{/if}}
import auth from './plugins/auth';
import catalog from './plugins/catalog';
import identity from './plugins/identity';
import scaffolder from './plugins/scaffolder';
import proxy from './plugins/proxy';
import techdocs from './plugins/techdocs';
import { PluginEnvironment } from './types';
function makeCreateEnv(loadedConfigs: AppConfig[]) {
const config = ConfigReader.fromConfigs(loadedConfigs);
return (plugin: string): PluginEnvironment => {
const logger = getRootLogger().child({ type: 'plugin', plugin });
{{#if dbTypePG}}
const knexConfig = {
client: 'pg',
useNullAsDefault: true,
connection: {
port: process.env.POSTGRES_PORT,
host: process.env.POSTGRES_HOST,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: `backstage_plugin_${plugin}`,
} as PgConnectionConfig,
};
{{/if}}
{{#if dbTypeSqlite}}
const knexConfig = {
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
};
{{/if}}
const database = knex(knexConfig);
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
resource.run('PRAGMA foreign_keys = ON', () => {});
});
return { logger, database, config };
};
}
async function main() {
const configs = await loadConfig();
const configReader = ConfigReader.fromConfigs(configs);
const createEnv = makeCreateEnv(configs);
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const identityEnv = useHotMemoize(module, () => createEnv('identity'));
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
const service = createServiceBuilder(module)
.loadConfig(configReader)
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
.addRouter('/auth', await auth(authEnv))
.addRouter('/identity', await identity(identityEnv))
.addRouter('/techdocs', await techdocs(techdocsEnv))
.addRouter('/proxy', await proxy(proxyEnv));
await service.start().catch(err => {
console.log(err);
process.exit(1);
});
}
module.hot?.accept();
main().catch(error => {
console.error(`Backend failed to start up, ${error}`);
process.exit(1);
});
@@ -0,0 +1,26 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouter } from '@backstage/plugin-auth-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
database,
config,
}: PluginEnvironment) {
return await createRouter({ logger, config, database });
}
@@ -0,0 +1,56 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createRouter,
DatabaseEntitiesCatalog,
DatabaseLocationsCatalog,
DatabaseManager,
HigherOrderOperations,
LocationReaders,
runPeriodically,
} from '@backstage/plugin-catalog-backend';
import { PluginEnvironment } from '../types';
import { useHotCleanup } from '@backstage/backend-common';
export default async function createPlugin({
logger,
database,
}: PluginEnvironment) {
const locationReader = new LocationReaders(logger);
const db = await DatabaseManager.createDatabase(database, { logger });
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
const locationsCatalog = new DatabaseLocationsCatalog(db);
const higherOrderOperation = new HigherOrderOperations(
entitiesCatalog,
locationsCatalog,
locationReader,
logger,
);
useHotCleanup(
module,
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000),
);
return await createRouter({
entitiesCatalog,
locationsCatalog,
higherOrderOperation,
logger,
});
}
@@ -0,0 +1,22 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouter } from '@backstage/plugin-identity-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({ logger }: PluginEnvironment) {
return await createRouter({ logger });
}
@@ -0,0 +1,25 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-ignore
import { createRouter } from '@backstage/plugin-proxy-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
config,
}: PluginEnvironment) {
return await createRouter({ logger, config });
}
@@ -0,0 +1,56 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
CookieCutter,
createRouter,
FilePreparer,
GithubPreparer,
Preparers,
GithubPublisher,
CreateReactAppTemplater,
Templaters,
} from '@backstage/plugin-scaffolder-backend';
import { Octokit } from '@octokit/rest';
import type { PluginEnvironment } from '../types';
import Docker from 'dockerode';
export default async function createPlugin({ logger }: PluginEnvironment) {
const cookiecutterTemplater = new CookieCutter();
const craTemplater = new CreateReactAppTemplater();
const templaters = new Templaters();
templaters.register('cookiecutter', cookiecutterTemplater);
templaters.register('cra', craTemplater);
const filePreparer = new FilePreparer();
const githubPreparer = new GithubPreparer();
const preparers = new Preparers();
preparers.register('file', filePreparer);
preparers.register('github', githubPreparer);
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
const publisher = new GithubPublisher({ client: githubClient });
const dockerClient = new Docker();
return await createRouter({
preparers,
templaters,
publisher,
logger,
dockerClient,
});
}
@@ -0,0 +1,22 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouter } from '@backstage/plugin-techdocs-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({ logger }: PluginEnvironment) {
return await createRouter({ logger });
}
@@ -0,0 +1,25 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Knex from 'knex';
import { Logger } from 'winston';
import { Config } from '@backstage/config';
export type PluginEnvironment = {
logger: Logger;
database: Knex;
config: Config;
};
@@ -1,8 +1,14 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": ["packages/*/src", "plugins/*/src", "plugins/*/dev"],
"exclude": ["**/node_modules"],
"include": [
"packages/*/src",
"plugins/*/src",
"plugins/*/dev",
"plugins/*/migrations"
],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "dist"
"outDir": "dist",
"skipLibCheck": true
}
}
@@ -36,7 +36,7 @@
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"jest-fetch-mock": "^3.0.3"
},
+1 -1
View File
@@ -36,7 +36,7 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/yup": "^0.28.2"
},
+1 -1
View File
@@ -33,7 +33,7 @@
"lodash": "^4.17.15"
},
"devDependencies": {
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0"
},
"files": [
+1 -1
View File
@@ -46,7 +46,7 @@
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/zen-observable": "^0.8.0",
"jest-fetch-mock": "^3.0.3"
@@ -17,7 +17,7 @@ import { createApiRef } from '../ApiRef';
import { Config } from '@backstage/config';
// Using interface to make the ConfigApi name show up in docs
export interface ConfigApi extends Config {}
export type ConfigApi = Config;
export const configApiRef = createApiRef<ConfigApi>({
id: 'core.config',
+1 -1
View File
@@ -61,7 +61,7 @@
"@testing-library/user-event": "^12.0.7",
"@types/classnames": "^2.2.9",
"@types/google-protobuf": "^3.7.2",
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/react-helmet": "^5.0.15",
"@types/zen-observable": "^0.8.0",
@@ -22,6 +22,7 @@ import {
oauth2ApiRef,
oktaAuthApiRef,
useApi,
configApiRef,
} from '@backstage/core-api';
import Collapse from '@material-ui/core/Collapse';
import SignOutIcon from '@material-ui/icons/MeetingRoom';
@@ -39,6 +40,9 @@ export function SidebarUserSettings() {
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(() => {
@@ -49,31 +53,41 @@ export function SidebarUserSettings() {
<>
<SidebarUserProfile open={open} setOpen={setOpen} />
<Collapse in={open} timeout="auto">
<OIDCProviderSettings
title="Google"
apiRef={googleAuthApiRef}
icon={Star}
/>
<OAuthProviderSettings
title="Github"
apiRef={githubAuthApiRef}
icon={Star}
/>
<OAuthProviderSettings
title="Gitlab"
apiRef={gitlabAuthApiRef}
icon={Star}
/>
<OIDCProviderSettings
title="Okta"
apiRef={oktaAuthApiRef}
icon={Star}
/>
<OIDCProviderSettings
title="YourOrg"
apiRef={oauth2ApiRef}
icon={Star}
/>
{providers.includes('google') && (
<OIDCProviderSettings
title="Google"
apiRef={googleAuthApiRef}
icon={Star}
/>
)}
{providers.includes('github') && (
<OAuthProviderSettings
title="Github"
apiRef={githubAuthApiRef}
icon={Star}
/>
)}
{providers.includes('gitlab') && (
<OAuthProviderSettings
title="Gitlab"
apiRef={gitlabAuthApiRef}
icon={Star}
/>
)}
{providers.includes('okta') && (
<OIDCProviderSettings
title="Okta"
apiRef={oktaAuthApiRef}
icon={Star}
/>
)}
{providers.includes('oauth2') && (
<OIDCProviderSettings
title="YourOrg"
apiRef={oauth2ApiRef}
icon={Star}
/>
)}
<SidebarItem
icon={SignOutIcon}
text="Sign Out"
@@ -33,19 +33,13 @@ import { SignInProvider } from './types';
const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider';
// Separate list here to avoid exporting internal types
export type SignInProviderId =
| 'guest'
| 'google'
| 'gitlab'
| 'custom'
| 'okta'
| 'github';
export type SignInProviderId = 'guest' | string;
const signInProviders: { [id in SignInProviderId]: SignInProvider } = {
guest: guestProvider,
google: googleProvider,
gitlab: gitlabProvider,
custom: customProvider,
oauth2: customProvider,
okta: oktaProvider,
github: githubProvider,
};
+1 -1
View File
@@ -46,7 +46,7 @@
"react-router-dom": "6.0.0-beta.0"
},
"devDependencies": {
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0"
},
"files": [
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
rules: {
'no-console': 0,
},
};
+21
View File
@@ -0,0 +1,21 @@
# DocGen - API Reference Documentation Generator
The docgen package provides a CLI to generate markdown documentation for all exported ApiRefs in `@backstage/core`. The documentation is generated based on exported `ApiRef` instances and their type parameters.
The CLI supports generating both TechDocs and GitHub Markdown, where the TechDocs one provides some better linking and syntax highlighting.
## Usage
To generate markdown documentation in the top-level `docs/` directory, run the following:
```bash
yarn docgen
```
## TODO
This package was lifted out from the Spotify internal Backstage project and could use some further work:
- Use a higher-level TypeScript compiler library, e.g. `ts-morph`.
- Support for generating docs for any package or multiple packages.
- Better handling of self-referencing types in APIs, e.g. ConfigApi.
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env node
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const path = require('path');
// Figure out whether we're running inside the backstage repo or as an installed dependency
const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src'));
if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) {
require('..');
} else {
require('ts-node').register({
transpileOnly: true,
compilerOptions: {
module: 'CommonJS',
},
});
require('../src');
}
+51
View File
@@ -0,0 +1,51 @@
{
"name": "docgen",
"description": "Tool for generating API Documentation for itself",
"version": "0.1.1-alpha.16",
"private": true,
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/spotify/backstage",
"directory": "packages/docgen"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"main": "src/index.ts",
"scripts": {
"build": "backstage-cli build --outputs cjs",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"docgen": "backstage-docgen generate --output ../../docs/reference/utility-apis --format github && prettier --write ../../docs/reference/utility-apis",
"clean": "backstage-cli clean",
"start": "nodemon --"
},
"bin": {
"backstage-docgen": "bin/backstage-docgen"
},
"dependencies": {
"chalk": "^4.0.0",
"commander": "^4.1.1",
"fs-extra": "^9.0.0",
"github-slugger": "^1.3.0",
"ts-node": "^8.6.2",
"typescript": "^3.9.3"
},
"devDependencies": {
"@types/fs-extra": "^9.0.1",
"@types/github-slugger": "^1.3.0",
"@types/node": "^13.7.2",
"nodemon": "^2.0.2"
},
"files": [
"bin",
"dist"
],
"nodemonConfig": {
"watch": "./src",
"exec": "bin/backstage-docgen",
"ext": "ts"
}
}
@@ -0,0 +1,267 @@
/*
* 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 ts from 'typescript';
import TypeLocator from './TypeLocator';
import { createMemProgram } from './testUtils';
import ApiDocGenerator from './ApiDocGenerator';
describe('ApiDocGenerator', () => {
it('should generate empty API doc', () => {
const program = createMemProgram(
`
import MyApi from './type';
type MyApiType = {};
export const myApi = new MyApi<MyApiType>({
id: 'my-id',
description: 'my-description',
});
`,
{
'/mem/type.ts': `export default class MyApi<T> {
constructor(private readonly info: { id: string, description: string }) {}
}`,
},
);
const typeLocator = TypeLocator.fromProgram(program, '/');
const { apiInstances } = typeLocator.findExportedInstances({
apiInstances: typeLocator.getExportedType('/mem/type.ts'),
});
expect(apiInstances.length).toBe(1);
const [apiInstance] = apiInstances;
const docGenerator = ApiDocGenerator.fromProgram(program, '/');
const doc = docGenerator.toDoc(apiInstance);
expect(doc.id).toBe('my-id');
expect(doc.description).toBe('my-description');
expect(doc.name).toBe('myApi');
expect(doc.file).toBe('mem/index.ts');
expect(doc.lineInFile).toBe(6);
expect(doc.interfaceInfos).toEqual([
{
dependentTypes: [],
docs: [],
file: 'mem/index.ts',
lineInFile: 4,
members: [],
name: 'MyApiType',
},
]);
});
it('should generate API docs', () => {
const program = createMemProgram(
`
import MyApi from './type';
/** MySubSubType Docs */
type MySubSubType = { n: number; };
/** MySubType Docs */
type MySubType = {
/** Field a docs */
a: boolean;
// Field b docs
b: MySubSubType;
}
/** MySecondSubType Docs */
/** With multiple comments */
export type MySecondSubType = { s: string };
// MyThirdSubType Docs that shouldn't show up
type MyThirdSubType = { b: boolean };
/** MyApiType Docs */
type MyApiType = {
/** Docs for x */
x: string;
// Line comments shouldn't show up
y: MySubType;
/** Multiple */
/** JsDoc */
/** Comments */
z(a: Promise<readonly [{k: MySecondSubType}[]]>): Array<MyThirdSubType>;
};
/** Should not show up */
export const myApi = new MyApi<MyApiType>({
id: 'my-id',
description: 'my-description',
});
`,
{
'/mem/type.ts': `export default class MyApi<T> {
constructor(private readonly info: { id: string, description: string }) {}
}`,
},
);
const source = program.getSourceFile('/mem/index.ts');
// Figure out type IDs so we can make sure they match later
const checker = program.getTypeChecker();
const symbols = checker.getSymbolsInScope(
source!.getChildren().slice(-1)[0],
ts.SymbolFlags.TypeAlias,
);
const Ids = [
'MySubType',
'MySubSubType',
'MySecondSubType',
'MyThirdSubType',
].reduce((ids, name) => {
const symbol = symbols.find(s => s.escapedName === name)!;
const type = checker.getTypeAtLocation(symbol.declarations[0]);
ids[name] = (type.aliasSymbol as any).id;
return ids;
}, {} as { [key in string]: number });
const typeLocator = TypeLocator.fromProgram(program, '/mem');
const { apiInstances } = typeLocator.findExportedInstances({
apiInstances: typeLocator.getExportedType('/mem/type.ts'),
});
expect(apiInstances.length).toBe(1);
const [apiInstance] = apiInstances;
const docGenerator = ApiDocGenerator.fromProgram(program, '/mem');
const doc = docGenerator.toDoc(apiInstance);
expect(doc.id).toBe('my-id');
expect(doc.description).toBe('my-description');
expect(doc.name).toBe('myApi');
expect(doc.file).toBe('index.ts');
expect(doc.interfaceInfos.length).toBe(1);
expect(doc.interfaceInfos[0].docs).toEqual(['MyApiType Docs']);
expect(doc.interfaceInfos[0].file).toBe('index.ts');
expect(doc.interfaceInfos[0].lineInFile).toBe(24);
expect(doc.interfaceInfos[0].name).toBe('MyApiType');
expect(doc.interfaceInfos[0].members).toEqual([
{
type: 'prop',
name: 'x',
path: 'MyApiType.x',
text: 'x: string',
docs: ['Docs for x'],
links: [],
},
{
type: 'prop',
name: 'y',
path: 'MyApiType.y',
text: 'y: MySubType',
docs: [],
links: [
{
id: Ids.MySubType,
name: 'MySubType',
path: 'index.ts/MySubType',
location: [3, 12],
},
],
},
{
type: 'method',
name: 'z',
path: 'MyApiType.z',
text:
'z(a: Promise<readonly [{k: MySecondSubType}[]]>): Array<MyThirdSubType>',
docs: ['Multiple', 'JsDoc', 'Comments'],
links: [
{
id: Ids.MySecondSubType,
name: 'MySecondSubType',
path: 'index.ts/MySecondSubType',
location: [27, 42],
},
{
id: Ids.MyThirdSubType,
name: 'MyThirdSubType',
path: 'index.ts/MyThirdSubType',
location: [56, 70],
},
],
},
]);
expect(doc.interfaceInfos[0].dependentTypes).toEqual([
{
id: Ids.MySubType,
name: 'MySubType',
path: 'index.ts/MySubType',
file: 'index.ts',
lineInFile: 8,
text: `type MySubType = {
/** Field a docs */
a: boolean;
// Field b docs
b: MySubSubType;
}`,
docs: ['MySubType Docs'],
links: [
{
id: Ids.MySubSubType,
name: 'MySubSubType',
path: 'index.ts/MySubSubType',
location: [102, 114],
},
],
children: [],
},
{
id: Ids.MySubSubType,
name: 'MySubSubType',
path: 'index.ts/MySubSubType',
file: 'index.ts',
lineInFile: 5,
text: 'type MySubSubType = { n: number; }',
docs: ['MySubSubType Docs'],
links: [],
children: [],
},
{
id: Ids.MySecondSubType,
name: 'MySecondSubType',
path: 'index.ts/MySecondSubType',
file: 'index.ts',
lineInFile: 18,
text: 'export type MySecondSubType = { s: string }',
docs: ['MySecondSubType Docs', 'With multiple comments'],
links: [],
children: [],
},
{
id: Ids.MyThirdSubType,
name: 'MyThirdSubType',
path: 'index.ts/MyThirdSubType',
file: 'index.ts',
lineInFile: 21,
text: 'type MyThirdSubType = { b: boolean }',
docs: [],
links: [],
children: [],
},
]);
});
});
@@ -0,0 +1,334 @@
/*
* 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 ts from 'typescript';
import { relative } from 'path';
import {
ExportedInstance,
ApiDoc,
InterfaceInfo,
FieldInfo,
TypeInfo,
TypeLink,
} from './types';
/**
* The ApiDocGenerator uses the typescript compiler API to build the data structure that
* describes a Backstage API and all of it's related types.
*
* It receives an exported instance that of the form
* `export name = createApiRef<Interface>({id: ..., description: ...})`.
* It will then traverse all fields and methods on the interface type, and also
* types and declaration of all types that are used by those, both directly and indirectly.
* While traversing, it collects information such as names, location in source, docs, etc.
*/
export default class ApiDocGenerator {
static fromProgram(program: ts.Program, sourcePath: string) {
return new ApiDocGenerator(program.getTypeChecker(), sourcePath);
}
constructor(
private readonly checker: ts.TypeChecker,
private readonly basePath: string,
) {}
/**
* Generate documentation information for a given exported symbol.
*/
toDoc(apiInstance: ExportedInstance): ApiDoc {
const { name, source, args, typeArgs } = apiInstance;
const [info] = args;
if (!ts.isObjectLiteralExpression(info)) {
throw new Error('api info is not an object literal');
}
const id = this.getObjectPropertyLiteral(info, 'id');
const description = this.getObjectPropertyLiteral(info, 'description');
const file = relative(this.basePath, source.fileName);
const { line } = source.getLineAndCharacterOfPosition(
apiInstance.node.getStart(),
);
const rootTypeNode = typeArgs[0];
const typeNodes = ts.isIntersectionTypeNode(rootTypeNode)
? rootTypeNode.types.slice()
: [rootTypeNode];
const interfaceInfos = typeNodes.map(typeNode =>
this.getInterfaceInfo(typeNode),
);
return {
id,
name,
file,
lineInFile: line + 1,
description,
interfaceInfos,
};
}
/**
* Grab jsDoc and regular comments for a node
*/
private getNodeDocs(node: ts.Node): string[] {
const docNodes = ((node && (node as any).jsDoc) || []) as ts.JSDoc[];
const docs = docNodes.map(docNode => docNode.comment || '').filter(Boolean);
return docs;
}
/**
* Collect information about a top-level type.
*/
private getInterfaceInfo(typeNode: ts.TypeNode): InterfaceInfo {
if (ts.isTypeQueryNode(typeNode)) {
throw new Error(
'APIs must have a proper type parameter, TypeQueries are now allowed',
);
}
if (!ts.isTypeReferenceNode(typeNode)) {
throw new Error('Interface is not a type node');
}
const type = this.checker.getTypeFromTypeNode(typeNode);
const interfaceMembers = type.symbol.members as Map<string, ts.Symbol>;
if (!interfaceMembers) {
throw new Error('Interface does not have any members');
}
const name = (type.aliasSymbol || type.symbol).name;
const [declaration] = (type.aliasSymbol || type.symbol).declarations;
const sourceFile = declaration.getSourceFile();
const file = relative(this.basePath, sourceFile.fileName);
const { line } = sourceFile.getLineAndCharacterOfPosition(
declaration.getStart(),
);
const docs = this.getNodeDocs(declaration);
const membersAndTypes = Array.from(
interfaceMembers.values(),
).flatMap(fieldSymbol => this.getMemberInfo(name, fieldSymbol));
const members = membersAndTypes.map(t => t.member);
const dependentTypes = this.flattenTypes(
membersAndTypes.flatMap(t => t.dependentTypes),
);
return { name, docs, file, lineInFile: line + 1, members, dependentTypes };
}
/**
* Grab primitive values from an object literal expression.
*/
private getObjectPropertyLiteral(
objectLiteral: ts.ObjectLiteralExpression,
propertyName: string,
): string {
const matchingProp = objectLiteral.properties.filter(
prop =>
ts.isPropertyAssignment(prop) &&
ts.isIdentifier(prop.name) &&
prop.name.text === propertyName,
)[0] as ts.PropertyAssignment;
if (!matchingProp) {
throw new Error(`no identifier found for property ${propertyName}`);
}
const { initializer } = matchingProp;
if (!ts.isStringLiteral(initializer)) {
throw new Error(`no string literal for ${propertyName}`);
}
return initializer.text;
}
/**
* Get field definitions for a given symbol.
*/
private getMemberInfo(
parentName: string,
symbol: ts.Symbol,
): { member: FieldInfo; dependentTypes: TypeInfo[] } {
const declaration = symbol.valueDeclaration;
const { links, infos: dependentTypes } = this.findAllTypeReferences(
declaration,
);
let type: FieldInfo['type'] = 'prop';
if (
ts.isPropertySignature(declaration) &&
declaration.type &&
ts.isFunctionTypeNode(declaration.type)
) {
type = 'method';
} else if (ts.isMethodSignature(declaration)) {
type = 'method';
}
return {
member: {
type,
name: symbol.name,
path: `${parentName}.${symbol.name}`,
text: declaration.getText().replace(/;$/, ''),
docs: this.getNodeDocs(declaration),
links: this.recalculateLinkOffsets(declaration, links),
},
dependentTypes,
};
}
/**
* Recursively search for references to types that we care about and want to include in the documentation.
*/
private findAllTypeReferences = (
node: ts.Node | undefined,
visited: Set<ts.Node> = new Set(),
): { links: TypeLink[]; infos: TypeInfo[] } => {
if (!node || visited.has(node)) {
return { links: [], infos: [] };
}
// This makes sure we don't end up in a loop.
// It doesn't exclude repeated types, e.g. Array<Array<MyType>>, since we're exiting on duplicate nodes, not types.
visited.add(node);
const children = node
.getChildren()
.flatMap(child => this.findAllTypeReferences(child, visited));
if (!ts.isTypeReferenceNode(node)) {
return {
links: children.flatMap(child => child.links),
infos: children.flatMap(child => child.infos),
};
}
const type = this.checker.getTypeFromTypeNode(node);
const info = this.validateTypeDeclaration(type);
if (!info) {
return {
links: children.flatMap(child => child.links),
infos: children.flatMap(child => child.infos),
};
}
const { symbol, declaration } = info;
const typeRefs = this.findAllTypeReferences(declaration, visited);
const sourceFile = declaration.getSourceFile();
const { line } = sourceFile.getLineAndCharacterOfPosition(
declaration.getStart(),
);
const file = relative(this.basePath, sourceFile.fileName);
const typeInfo = {
id: (symbol as any).id,
name: symbol.name,
text: declaration.getText().replace(/;$/, ''),
docs: this.getNodeDocs(declaration),
path: `${file}/${symbol.name}`,
file,
lineInFile: line + 1, // TS line is 0-based
links: this.recalculateLinkOffsets(declaration, typeRefs.links),
children: typeRefs.infos,
};
const link = {
id: typeInfo.id,
path: typeInfo.path,
name: typeInfo.name,
location: [node.typeName.getStart(), node.typeName.getEnd()] as const,
};
return {
links: [link, ...children.flatMap(child => child.links)],
infos: [typeInfo, ...children.flatMap(child => child.infos)],
};
};
/**
* Check if a given type declaration is one that we care about.
*/
private validateTypeDeclaration(
type: ts.Type | undefined,
): undefined | { symbol: ts.Symbol; declaration: ts.Declaration } {
if (!type) {
return undefined;
}
const symbol = type.aliasSymbol || type.symbol;
if (!symbol) {
return undefined;
}
const [declaration] = symbol.declarations;
// Don't generate standalone infos for type paramters
if (ts.isTypeParameterDeclaration(declaration)) {
return undefined;
}
if (declaration.getSourceFile().fileName.includes('node_modules')) {
return undefined;
}
return { symbol, declaration };
}
/**
* Flatten a tree of TypeInfos with children into a flat
* list of TypeInfos with duplicates removed.
*/
private flattenTypes(descs: TypeInfo[]): TypeInfo[] {
function getChildren(parent: TypeInfo): TypeInfo[] {
return [
{
...parent,
children: [],
},
...parent.children.flatMap(getChildren),
];
}
const flatDescs = descs.flatMap(getChildren);
const seenTypes = new Set<number>();
return flatDescs.filter(desc => {
if (seenTypes.has(desc.id)) {
return false;
}
seenTypes.add(desc.id);
return true;
});
}
/**
* Calculate link positions within a block of code, with 0 being
* the first character in the block.
*/
private recalculateLinkOffsets(
parent: ts.Node,
links: TypeLink[],
): TypeLink[] {
const parentStart = parent.getStart();
return links.map(link => {
const [start, end] = link.location;
return {
...link,
location: [start - parentStart, end - parentStart],
};
});
}
}
@@ -0,0 +1,163 @@
/*
* 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 sortSelector from './sortSelector';
import { ApiDoc, InterfaceInfo, MarkdownPrinter } from './types';
/**
* The ApiDocPrinter takes a ApiDoc data structure, typically generated by an ApiDocGenerator,
* and prints it out as a markdown doc with custom code highlighting and links.
*/
export default class ApiDocPrinter {
printerFactory: () => MarkdownPrinter;
constructor(printerFactory: () => MarkdownPrinter) {
this.printerFactory = printerFactory;
}
/**
* Print an index file with all ApiRefs and what types they implement.
*/
printApiIndex(apiDocs: ApiDoc[]): Buffer {
const printer = this.printerFactory();
printer.header(1, 'Backstage Core Utility APIs');
printer.paragraph(
'The following is a list of all Utility APIs defined by `@backstage/core`.',
'They are available to use by plugins and components, and can be accessed ',
'using the `useApi` hook, also provided by `@backstage/core`.',
'For more information, see https://github.com/spotify/backstage/blob/master/docs/api/utility-apis.md.',
);
for (const api of apiDocs) {
printer.header(3, `${this.apiDisplayName(api)}`, api.id);
printer.paragraph(api.description);
const typeLinks = api.interfaceInfos.map(
i => `[${i.name}](${printer.pageLink(i.name)})`,
);
printer.paragraph(
`Implemented type${typeLinks.length > 1 ? 's' : ''}: ${typeLinks.join(
', ',
)}`,
);
printer.paragraph(`ApiRef: ${printer.srcLink(api, api.name)}`);
}
return printer.toBuffer();
}
/**
* Print documentation page for a type implemented by an ApiRef and
*/
printInterface(apiType: InterfaceInfo, apiDocs: ApiDoc[]): Buffer {
const printer = this.printerFactory();
printer.header(1, apiType.name);
printer.paragraph(
`The ${apiType.name} type is defined at ${printer.srcLink(apiType)}.`,
);
const apiLinks = apiDocs
.filter(ad => ad.interfaceInfos.some(i => i.name === apiType.name))
.map(ad => {
const link =
printer.indexLink() +
printer.headerLink(this.apiDisplayName(ad), ad.id);
return `[${ad.name}](${link})`;
});
if (apiLinks.length === 1) {
printer.paragraph(
`The following Utility API implements this type: ${apiLinks}`,
);
} else {
printer.paragraph(`The following Utility APIs implement this type:`);
for (const link of apiLinks) {
printer.text(` - ${link}`);
}
}
printer.header(2, 'Members');
this.addInterfaceMembers(printer, apiType);
if (apiType.dependentTypes.length) {
printer.header(2, 'Supporting types');
printer.paragraph(
'These types are part of the API declaration, but may not be unique to this API.',
);
this.addInterfaceTypes(printer, apiType);
}
return printer.toBuffer();
}
private addInterfaceMembers(
printer: MarkdownPrinter,
apiType: InterfaceInfo,
) {
for (const member of apiType.members) {
printer.header(
3,
`${member.name}${member.type === 'method' ? '()' : ''}`,
member.path,
);
for (const doc of member.docs) {
printer.text(doc);
}
printer.code(member);
}
}
private addInterfaceTypes(printer: MarkdownPrinter, apiType: InterfaceInfo) {
for (const type of apiType.dependentTypes
.slice()
.sort(sortSelector(x => x.name))) {
printer.header(3, `${type.name}`, type.path);
for (const doc of type.docs) {
printer.text(doc);
}
printer.code(type);
printer.paragraph(`Defined at ${printer.srcLink(type)}.`);
const usageLinks = [...apiType.members, ...apiType.dependentTypes]
.filter(member => {
return member.links.some(link => link.id === type.id);
})
.map(
({ name, path }) => `[${name}](${printer.headerLink(name, path)})`,
);
if (usageLinks.length) {
printer.paragraph(`Referenced by: ${usageLinks.join(', ')}.`);
}
}
}
private apiDisplayName(api: ApiDoc): string {
return api.name.replace(/ApiRef$/, '');
}
}
@@ -0,0 +1,145 @@
/*
* 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 GithubSlugger from 'github-slugger';
import sortSelector from './sortSelector';
import { MarkdownPrinter, TypeLink } from './types';
import { execSync } from 'child_process';
// TODO(Rugvip): provide through options?
const GH_BASE_URL = 'https://github.com/spotify/backstage';
const COMMIT_SHA =
process.env.COMMIT_SHA ||
execSync('git rev-parse HEAD').toString('utf8').trim();
/**
* The GithubMarkdownPrinter is a MarkdownPrinter for printing Github-flavored markdown documents.
*/
export default class GithubMarkdownPrinter implements MarkdownPrinter {
private str: string = '';
private line(line: string = '') {
this.str += `${line}\n`;
}
text(text: string) {
this.line(text);
this.line();
}
header(level: number, text: string) {
this.line(`${'#'.repeat(level)} ${text}`);
this.line();
}
headerLink(heading: string): string {
const slug = GithubSlugger.slug(heading);
return `#${slug}`;
}
indexLink() {
return './README.md';
}
pageLink(name: string) {
return `./${name}.md`;
}
srcLink(
{ file, lineInFile }: { file: string; lineInFile: number },
text?: string,
): string {
const linkText = text ?? `${file}:${lineInFile}`;
const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`;
return `[${linkText}](${href})`;
}
paragraph(...text: string[]) {
this.line(
text
.join('\n')
.trim()
.split('\n')
.map(line => line.trim())
.join('\n'),
);
this.line();
}
code({ text, links = [] }: { text: string; links?: TypeLink[] }) {
this.line('<pre>');
this.line(this.formatWithLinks({ text, links }));
this.line('</pre>');
this.line();
}
private escapeText: (text: string) => string = (() => {
const escapes: { [char in string]: string } = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
};
return (text: string) => text.replace(/[&<>]/g, char => escapes[char]);
})();
private formatWithLinks({
text,
links = [],
}: {
text: string;
links?: TypeLink[];
}): string {
const sortedLinks = links.slice().sort(sortSelector(x => x.location[0]));
sortedLinks.reduce((lastEnd, link) => {
if (link.location[0] <= lastEnd) {
throw new Error(
`overlapping link detected for ${link.path}, ${link.location}`,
);
}
return link.location[1];
}, -1);
const parts: Array<{ text: string; link?: boolean }> = [];
const endLocation = sortedLinks.reduce((prev, link) => {
const [start, end] = link.location;
parts.push(
{ text: text.slice(prev, start) },
{ text: text.slice(start, end), link: true },
);
return end;
}, 0);
parts.push({ text: text.slice(endLocation) });
return parts
.map(part => {
if (part.link) {
const link = this.headerLink(part.text);
return `<a href="${link}">${this.escapeText(part.text)}</a>`;
}
return this.escapeText(part.text);
})
.join('');
}
toBuffer(): Buffer {
return Buffer.from(this.str, 'utf8');
}
}
@@ -0,0 +1,161 @@
/*
* 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 sortSelector from './sortSelector';
import { Highlighter, MarkdownPrinter, TypeLink } from './types';
import { execSync } from 'child_process';
// TODO(Rugvip): provide through options?
const GH_BASE_URL = 'https://github.com/spotify/backstage';
const COMMIT_SHA =
process.env.COMMIT_SHA || execSync('git rev-parse HEAD').toString('utf8');
/**
* The TechdocsMarkdownPrinter is a MarkdownPrinter for building TechDocs markdown documents.
*/
export default class TechdocsMarkdownPrinter implements MarkdownPrinter {
private str: string = '';
private readonly highlighter: Highlighter;
constructor(highlighter: Highlighter) {
this.highlighter = highlighter;
// Remove line numbers from codeblocks
this.style('.linenodiv{ display: none }');
}
private line(line: string = '') {
this.str += `${line}\n`;
}
text(text: string) {
this.line(text);
this.line();
}
style(style: string) {
this.line('<style>');
this.line(style);
this.line('</style>');
}
header(level: number, text: string, id?: string) {
this.line(`${'#'.repeat(level)} ${text}${id ? ` {#${id}}` : ''}`);
this.line();
}
headerLink(heading: string, id?: string): string {
return `#${id ?? heading}`;
}
indexLink() {
return '../';
}
pageLink(name: string) {
return `./${name}/`;
}
srcLink(
{ file, lineInFile }: { file: string; lineInFile: number },
text?: string,
): string {
const linkText = text ?? `${file}:${lineInFile}`;
const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`;
return `[${linkText}](${href})`;
}
paragraph(...text: string[]) {
this.line(
text
.join('\n')
.trim()
.split('\n')
.map(line => line.trim())
.join('\n'),
);
this.line();
}
code({ text, links = [] }: { text: string; links?: TypeLink[] }) {
this.line('<div class="code">');
this.line(
'<div class="codehilite" style="background: #f0f0f0; padding: 0.225rem 0.6rem">',
);
this.line('<pre style="line-height: 125%">');
this.line(this.formatWithLinks({ text, links }));
this.line('</pre>');
this.line('</div>');
this.line('</div>');
}
private escapeText: (text: string) => string = (() => {
const escapes: { [char in string]: string } = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
};
return (text: string) => text.replace(/[&<>]/g, char => escapes[char]);
})();
private formatWithLinks({
text,
links = [],
}: {
text: string;
links?: TypeLink[];
}): string {
const sortedLinks = links.slice().sort(sortSelector(x => x.location[0]));
sortedLinks.reduce((lastEnd, link) => {
if (link.location[0] <= lastEnd) {
throw new Error(
`overlapping link detected for ${link.path}, ${link.location}`,
);
}
return link.location[1];
}, -1);
const parts: Array<{ text: string; path?: string }> = [];
const endLocation = sortedLinks.reduce((prev, link) => {
const [start, end] = link.location;
parts.push(
{ text: text.slice(prev, start) },
{ text: text.slice(start, end), path: link.path },
);
return end;
}, 0);
parts.push({ text: text.slice(endLocation) });
return parts
.map(part => {
if (part.path) {
const link = this.headerLink(part.text, part.path);
return `<a href="${link}">${this.escapeText(part.text)}</a>`;
}
return this.highlighter.highlight(this.escapeText(part.text));
})
.join('');
}
toBuffer(): Buffer {
return Buffer.from(this.str, 'utf8');
}
}
@@ -0,0 +1,68 @@
/*
* 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 ts from 'typescript';
import TypeLocator from './TypeLocator';
import { createMemProgram } from './testUtils';
describe('TypeLocator', () => {
it('should find a default export', () => {
const program = createMemProgram('export default class MyApi<T> {}');
const typeLocator = TypeLocator.fromProgram(program, '/mem');
const apiType = typeLocator.getExportedType('/mem/index.ts');
expect(apiType.symbol.name).toBe('default');
const [declaration] = apiType.symbol.declarations;
expect(declaration.kind).toBe(ts.SyntaxKind.ClassDeclaration);
expect((declaration as ts.ClassDeclaration).name).toBeDefined();
expect((declaration as ts.ClassDeclaration).name!.text).toBe('MyApi');
}, 10000);
it('should find api instance export', () => {
const program = createMemProgram(
`
import MyApi from './type';
type MyApiType = {};
export const myApi = new MyApi<MyApiType>();
`,
{
'/mem/type.ts': 'export default class MyApi<T> {}',
},
);
const typeLocator = TypeLocator.fromProgram(program, '/mem');
const { apiInstances } = typeLocator.findExportedInstances({
apiInstances: typeLocator.getExportedType('/mem/type.ts'),
});
expect(apiInstances.length).toBe(1);
const [apiInstance] = apiInstances;
expect(apiInstance.name).toBe('myApi');
expect(apiInstance.source.fileName).toBe('/mem/index.ts');
expect(apiInstance.args).toEqual([]);
expect(apiInstance.typeArgs.length).toBe(1);
const [typeArg] = apiInstance.typeArgs;
expect(typeArg.kind).toBe(ts.SyntaxKind.TypeReference);
expect((typeArg as ts.TypeReferenceNode).typeName.getText()).toBe(
'MyApiType',
);
});
});
+142
View File
@@ -0,0 +1,142 @@
/*
* 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 ts from 'typescript';
import { ExportedInstance } from './types';
/**
* The TypeLocator is used to extract typescrint Type structures from a compiled program,
* as well as finding locations where types are used in according to a matching pattern.
*
* This is used to e.g. find exported APIs that we should generate documentation for.
*/
export default class TypeLocator {
static fromProgram(program: ts.Program, sourcePath: string) {
return new TypeLocator(program.getTypeChecker(), program, sourcePath);
}
constructor(
private readonly checker: ts.TypeChecker,
private readonly program: ts.Program,
private readonly sourcePath: string,
) {}
/**
* Get the type of a symbol by export path and name
*/
getExportedType(path: string, exportedName: string = 'default'): ts.Type {
const source = this.program.getSourceFile(path);
if (!source) {
throw new Error(`Source not found for path '${path}'`);
}
const exported = this.checker.getExportsOfModule(
this.checker.getSymbolAtLocation(source)!,
);
const [symbol] = exported.filter(e => e.name === exportedName);
if (!symbol) {
throw new Error(`No export '${exportedName}' found in ${path}`);
}
const type = this.checker.getTypeOfSymbolAtLocation(symbol, source);
return type;
}
/**
* Find exported instances and return values from calls using the types
* provided in the lookup table.
*/
findExportedInstances<T extends string>(
typeLookupTable: { [key in T]: ts.Type },
): { [key in T]: ExportedInstance[] } {
const docMap = new Map<ts.Type, ExportedInstance[]>();
for (const type of Object.values<ts.Type>(typeLookupTable)) {
docMap.set(type, []);
}
this.program.getSourceFiles().forEach(source => {
const inRoot = source.fileName.startsWith(this.sourcePath);
if (!inRoot) {
return;
}
ts.forEachChild(source, node => {
const decl = this.getExportedConstructorDeclaration(node);
if (!decl || !docMap.has(decl.constructorType)) {
return;
}
docMap.get(decl.constructorType)!.push({
node,
name: decl.name,
source,
args: Array.from(decl.initializer.arguments || []),
typeArgs: Array.from(decl.initializer.typeArguments || []),
});
});
});
const result: { [key in T]?: ExportedInstance[] } = {};
for (const key in typeLookupTable) {
if (typeLookupTable.hasOwnProperty(key)) {
const type = typeLookupTable[key];
result[key] = docMap.get(type)!;
}
}
return result as { [key in T]: ExportedInstance[] };
}
private getExportedConstructorDeclaration(
node: ts.Node,
):
| {
constructorType: ts.Type;
initializer: ts.CallExpression | ts.NewExpression;
name: string;
}
| undefined {
if (!ts.isVariableStatement(node)) {
return undefined;
}
if (
!node.modifiers ||
!node.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)
) {
return undefined;
}
const { declarations } = node.declarationList;
if (declarations.length !== 1) {
return undefined;
}
const [declaration] = declarations;
const { initializer, name } = declaration;
if (!initializer || !name) {
return undefined;
}
if (!ts.isCallOrNewExpression(initializer)) {
return undefined;
}
if (!ts.isIdentifier(name)) {
return undefined;
}
const constructorType = this.checker.getTypeAtLocation(
initializer.expression,
);
return { constructorType, initializer, name: name.text };
}
}
@@ -0,0 +1,87 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Highlighter } from './types';
// Simple syntax highlighter that mimics hilite
export default class TypescriptHighlighter implements Highlighter {
private static readonly basicTypes = [
'boolean',
'number',
'string',
'Array',
'object',
'Record',
'Set',
'Map',
'true',
'false',
'null',
'undefined',
'void',
'Promise',
'any',
'[0-9\\.]+',
];
// List of highlightings to apply, each with a match regex and the style that should be applied
private static readonly highlighters = [
[/(\/\*\*?(?:.|\n)+?\*\/)/g, 'color: #60a0b0; font-style: italic'], // block comment
[/(\/\/.*)/gm, 'color: #60a0b0; font-style: italic'], // line comment
[/('[^']*?'|"[^"]*?")/g, 'color: #4070a0'], // string literals
[
new RegExp(
`\\b(${TypescriptHighlighter.basicTypes.join('|')})\\b(?!:|,)`,
'g',
),
'color: #902000',
], // basic types
[
/^((?:export\s)?(?:type\s)?(?:interface\s)?)/g,
'color: #007020; font-weight: bold',
], // keywords
] as readonly [RegExp, string][];
highlight(fullText: string): string {
// Each part is either plain text that can be highlighted or text that is already highlighted
type HighlightPart = { text: string; highlighted?: boolean };
const painter = (regex: RegExp, style: string) => (part: HighlightPart) => {
if (part.highlighted) {
return [part];
}
// Apply highlighting to all matches of the regex by splitting into parts
return part.text.split(regex).map((text, index) => {
// Odd parts are the ones that matched the regex and should be highlighted.
if (index % 2 === 1) {
return {
text: `<span style="${style}">${text}</span>`,
highlighted: true,
};
}
return { text };
});
};
// Order here is important, e.g. comments must be first to avoid string literals inside comments being highlighted
return TypescriptHighlighter.highlighters
.reduce((parts, highlighter) => parts.flatMap(painter(...highlighter)), [
{ text: fullText },
])
.map(({ text }) => text)
.join('');
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import sortSelector from './sortSelector';
describe('sortSelector', () => {
it('should stable sort', () => {
const arr = [
[3, 1],
[1, 2],
[1, 1],
[1, 3],
[2, 1],
];
const sortedByFirst = arr.slice().sort(sortSelector(([first]) => first));
expect(sortedByFirst).toEqual([
[1, 2],
[1, 1],
[1, 3],
[2, 1],
[3, 1],
]);
const sortedBySecond = arr
.slice()
.sort(sortSelector(([, second]) => second));
expect(sortedBySecond).toEqual([
[3, 1],
[1, 1],
[2, 1],
[1, 2],
[1, 3],
]);
});
});
@@ -0,0 +1,33 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* The sortSelector is a utility that makes a sort function by selecting the value to sort on with a lambda.
*/
export default function sortSelector<T>(
selector: (x: T) => any,
): (a: T, b: T) => -1 | 1 | 0 {
return (a: T, b: T) => {
const aV = selector(a);
const bV = selector(b);
if (aV < bV) {
return -1;
} else if (aV > bV) {
return 1;
}
return 0;
};
}
+67
View File
@@ -0,0 +1,67 @@
/*
* 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 ts from 'typescript';
export function createMemProgram(
indexSource: string,
otherSourceFiles: { [fileName in string]: string } = {},
): ts.Program {
const rootDir = '/mem';
const options = { noEmit: true };
const baseHost = ts.createCompilerHost(options);
const files: { [fileName in string]: string } = {
[`${rootDir}/index.ts`]: indexSource,
...otherSourceFiles,
};
// Custom compiler hosts that reads from a map of in-memory files, but
// falls back to reading from disc for ts libs etc.
const compilerHost: ts.CompilerHost = {
...baseHost,
readFile(fileName): string | undefined {
if (fileName in files) {
return files[fileName];
}
return baseHost.readFile(fileName);
},
getCurrentDirectory(): string {
return rootDir;
},
directoryExists(dir) {
if (dir === rootDir) {
return true;
}
if (baseHost.directoryExists) {
return baseHost.directoryExists(dir);
}
return false;
},
fileExists(fileName: string): boolean {
return !!files[fileName] || baseHost.fileExists(fileName);
},
getSourceFile(fileName, ...rest): ts.SourceFile | undefined {
const file = files[fileName];
if (file) {
return ts.createSourceFile(fileName, file, ts.ScriptTarget.ES2017);
}
return baseHost.getSourceFile(fileName, ...rest);
},
};
return ts.createProgram(['/mem/index.ts'], options, compilerHost);
}
+119
View File
@@ -0,0 +1,119 @@
/*
* 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 ts from 'typescript';
/**
* A TypeLink is a link to a different type.
*/
export type TypeLink = {
// The ID of the linked type
id: number;
// The path to the type from the project root
path: string;
// The name of the type, to display
name: string;
// The location of the type name as it appears in it's parent text.
location: readonly [number, number];
};
/**
* TypeInfo describes a Typescript Type.
*/
export type TypeInfo = {
id: number;
name: string;
path: string;
file: string;
lineInFile: number;
text: string;
docs: string[];
links: TypeLink[];
children: TypeInfo[];
};
/**
* FieldInfo describes a property or method in a documented API.
*/
export type FieldInfo = {
type: 'prop' | 'method';
name: string;
path: string;
text: string;
docs: string[];
links: TypeLink[];
};
/**
* InterfaceInfo describes the type of a documented API.
*/
export type InterfaceInfo = {
name: string;
docs: string[];
file: string;
lineInFile: number;
members: Array<FieldInfo>;
dependentTypes: TypeInfo[];
};
/**
* ApiDoc describes a documented API.
*/
export type ApiDoc = {
id: string;
name: string;
description: string;
file: string;
lineInFile: number;
interfaceInfos: InterfaceInfo[];
};
/**
* ExportedInstance describes an expression matching `export {name} = new {Contructor}<{typeArgs}...>({args}...)`
*/
export type ExportedInstance = {
node: ts.Node;
name: string;
source: ts.SourceFile;
args: Array<ts.Expression>;
typeArgs: Array<ts.TypeNode>;
};
export type Highlighter = {
highlight(test: string): string;
};
/**
* Markdown printer is an abstraction for printing markdown documents of different flavors.
*/
export type MarkdownPrinter = {
text(text: string): void;
header(level: number, text: string, id?: string): void;
paragraph(...text: string[]): void;
code(options: { text: string; links?: TypeLink[] }): void;
headerLink(header: string, id?: string): string;
/** Link from pages to index */
indexLink(): string;
/** Link from index to pages */
pageLink(name: string): string;
srcLink(
{ file, lineInFile }: { file: string; lineInFile: number },
text?: string,
): string;
toBuffer(): Buffer;
};
+126
View File
@@ -0,0 +1,126 @@
/*
* 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 * as ts from 'typescript';
import fs from 'fs-extra';
import { resolve as resolvePath, join as joinPath } from 'path';
import ApiDocGenerator from './docgen/ApiDocGenerator';
import sortSelector from './docgen/sortSelector';
import TypeLocator from './docgen/TypeLocator';
import ApiDocPrinter from './docgen/ApiDocsPrinter';
import TypescriptHighlighter from './docgen/TypescriptHighlighter';
import GitHubMarkdownPrinter from './docgen/GitHubMarkdownPrinter';
import TechdocsMarkdownPrinter from './docgen/TechdocsMarkdownPrinter';
const FORMATS = ['github', 'techdocs'] as const;
export async function generate(
targetPath: string,
format: typeof FORMATS[number],
) {
if (!FORMATS.includes(format)) {
throw new TypeError(
`Invalid format, '${format}', must be one of ${FORMATS.join(', ')}`,
);
}
const rootDir = resolvePath(__dirname, '../../..');
const srcDir = resolvePath(rootDir, 'packages', 'core-api', 'src');
const targetDir = resolvePath(targetPath);
const options = await fs.readJson(resolvePath('../cli/config/tsconfig.json'));
delete options.moduleResolution;
options.noEmit = true;
const program = ts.createProgram([resolvePath(srcDir, 'index.ts')], options);
const typeLocator = TypeLocator.fromProgram(program, srcDir);
const { apis } = typeLocator.findExportedInstances({
apis: typeLocator.getExportedType(
resolvePath(srcDir, 'index.ts'),
'createApiRef',
),
});
const apiDocGenerator = ApiDocGenerator.fromProgram(program, rootDir);
const apiDocs = apis
.map(api => {
try {
return apiDocGenerator.toDoc(api);
} catch (error) {
throw new Error(
`Doc generation failed for API in ${api.source.fileName}, ${error.stack}`,
);
}
})
.sort(sortSelector(x => x.name));
const apiTypes = Object.values(
Object.fromEntries(
apiDocs.flatMap(d => d.interfaceInfos).map(i => [i.name, i]),
),
).sort(sortSelector(i => i.name));
if (format === 'techdocs') {
const docsDir = resolvePath(targetDir, 'docs');
await fs.ensureDir(docsDir);
const apiDocPrinter = new ApiDocPrinter(
() => new TechdocsMarkdownPrinter(new TypescriptHighlighter()),
);
await fs.writeFile(
joinPath(docsDir, 'README.md'),
apiDocPrinter.printApiIndex(apiDocs),
);
for (const apiType of Object.values(apiTypes)) {
const data = apiDocPrinter.printInterface(apiType, apiDocs);
await fs.writeFile(joinPath(docsDir, `${apiType.name}.md`), data);
}
await fs.writeFile(
resolvePath(targetDir, 'mkdocs.yml'),
[
'site_name: Backstage Core Utility API References',
'nav:',
` - API Index: 'README.md'`,
...apiTypes.map(({ name }) => ` - ${name}: '${name}.md'`),
'plugins:',
' - techdocs-core',
].join('\n'),
'utf8',
);
} else {
await fs.ensureDir(targetDir);
const apiDocPrinter = new ApiDocPrinter(() => new GitHubMarkdownPrinter());
await fs.writeFile(
joinPath(targetDir, 'README.md'),
apiDocPrinter.printApiIndex(apiDocs),
);
for (const apiType of Object.values(apiTypes)) {
const data = apiDocPrinter.printInterface(apiType, apiDocs);
await fs.writeFile(joinPath(targetDir, `${apiType.name}.md`), data);
}
}
}
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import program from 'commander';
import { resolve as resolvePath } from 'path';
import chalk from 'chalk';
import fs from 'fs-extra';
import { generate } from './generate';
const main = (argv: string[]) => {
const pkgJson = fs.readJsonSync(resolvePath(__dirname, '../package.json'));
program.name('docgen').version(pkgJson.version);
program
.command('generate')
.description(
'Generate documentation for the declarations in the core-api package',
)
.option('--output <output>', 'Output directory [./dist]')
.option(
'--format <format>',
'Output format, either techdocs or github [techdocs]',
)
.action(async cmd => {
await generate(cmd.output ?? './dist', cmd.format ?? 'techdocs');
});
program.on('command:*', () => {
console.log();
console.log(
chalk.red(`Invalid command: ${chalk.cyan(program.args.join(' '))}`),
);
console.log(chalk.red('See --help for a list of available commands.'));
console.log();
process.exit(1);
});
if (!process.argv.slice(2).length) {
program.outputHelp(chalk.yellow);
}
program.parse(argv);
};
process.on('unhandledRejection', rejection => {
console.error(String(rejection));
process.exit(1);
});
main(process.argv);
+4
View File
@@ -18,10 +18,14 @@ import {
OAuthRequestManager,
OktaAuth,
oktaAuthApiRef,
configApiRef,
ConfigReader,
} from '@backstage/core';
const builder = ApiRegistry.builder();
builder.add(configApiRef, ConfigReader.fromConfigs([]));
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder()));
+1 -1
View File
@@ -36,7 +36,7 @@
"react-dom": "^16.12.0"
},
"devDependencies": {
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0"
},
"files": [
+1 -1
View File
@@ -45,7 +45,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@types/jest": "^25.2.2",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0"
},
"files": [
@@ -33,7 +33,7 @@ export class EnvironmentHandler implements AuthProviderRouteHandlers {
): AuthProviderRouteHandlers | undefined {
const env = req.query.env?.toString();
if (this.providers.hasOwnProperty(env)) {
if (env && this.providers.hasOwnProperty(env)) {
return this.providers[env];
}
+26 -10
View File
@@ -24,6 +24,11 @@ import { createOAuth2Provider } from './oauth2';
import { createOktaProvider } from './okta';
import { createSamlProvider } from './saml';
import { AuthProviderConfig, AuthProviderFactory } from './types';
import { Config } from '@backstage/config';
import {
EnvironmentHandlers,
EnvironmentHandler,
} from '../lib/EnvironmentHandler';
const factories: { [providerId: string]: AuthProviderFactory } = {
google: createGoogleProvider,
@@ -37,7 +42,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
export const createAuthProviderRouter = (
providerId: string,
globalConfig: AuthProviderConfig,
providerConfig: any, // TODO: make this a config reader object of sorts
providerConfig: Config,
logger: Logger,
issuer: TokenIssuer,
) => {
@@ -46,17 +51,28 @@ export const createAuthProviderRouter = (
throw Error(`No auth provider available for '${providerId}'`);
}
const provider = factory(globalConfig, providerConfig, logger, issuer);
const router = Router();
router.get('/start', provider.start.bind(provider));
router.get('/handler/frame', provider.frameHandler.bind(provider));
router.post('/handler/frame', provider.frameHandler.bind(provider));
if (provider.logout) {
router.post('/logout', provider.logout.bind(provider));
const envs = providerConfig.keys();
const envProviders: EnvironmentHandlers = {};
for (const env of envs) {
const envConfig = providerConfig.getConfig(env);
const provider = factory(globalConfig, env, envConfig, logger, issuer);
if (provider) {
envProviders[env] = provider;
}
}
if (provider.refresh) {
router.get('/refresh', provider.refresh.bind(provider));
const handler = new EnvironmentHandler(providerId, envProviders);
router.get('/start', handler.start.bind(handler));
router.get('/handler/frame', handler.frameHandler.bind(handler));
router.post('/handler/frame', handler.frameHandler.bind(handler));
if (handler.logout) {
router.post('/logout', handler.logout.bind(handler));
}
if (handler.refresh) {
router.get('/refresh', handler.refresh.bind(handler));
}
return router;
@@ -25,20 +25,15 @@ import {
OAuthProviderHandlers,
AuthProviderConfig,
RedirectInfo,
EnvironmentProviderConfig,
OAuthProviderOptions,
OAuthProviderConfig,
OAuthResponse,
PassportDoneCallback,
} from '../types';
import { OAuthProvider } from '../../lib/OAuthProvider';
import {
EnvironmentHandlers,
EnvironmentHandler,
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import passport from 'passport';
import { Config } from '@backstage/config';
export class GithubAuthProvider implements OAuthProviderHandlers {
private readonly _strategy: GithubStrategy;
@@ -131,44 +126,43 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
export function createGithubProvider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
env: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'github';
const envProviders: EnvironmentHandlers = {};
const secure = envConfig.getBoolean('secure');
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as OAuthProviderConfig;
const { secure, appOrigin } = config;
const opts = {
clientID: config.clientId,
clientSecret: config.clientSecret,
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
};
const opts = {
clientID,
clientSecret,
callbackURL,
};
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars',
);
}
logger.warn(
'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable',
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars',
);
continue;
}
envProviders[env] = new OAuthProvider(new GithubAuthProvider(opts), {
disableRefresh: true,
persistScopes: true,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
logger.warn(
'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable',
);
return undefined;
}
return new EnvironmentHandler(providerId, envProviders);
return new OAuthProvider(new GithubAuthProvider(opts), {
disableRefresh: true,
persistScopes: true,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
}
@@ -25,20 +25,15 @@ import {
OAuthProviderHandlers,
AuthProviderConfig,
RedirectInfo,
EnvironmentProviderConfig,
OAuthProviderOptions,
OAuthProviderConfig,
OAuthResponse,
PassportDoneCallback,
} from '../types';
import { OAuthProvider } from '../../lib/OAuthProvider';
import {
EnvironmentHandlers,
EnvironmentHandler,
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import passport from 'passport';
import { Config } from '@backstage/config';
export class GitlabAuthProvider implements OAuthProviderHandlers {
private readonly _strategy: GitlabStrategy;
@@ -138,49 +133,45 @@ export class GitlabAuthProvider implements OAuthProviderHandlers {
export function createGitlabProvider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
env: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'gitlab';
const envProviders: EnvironmentHandlers = {};
const secure = envConfig.getBoolean('secure');
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const audience = envConfig.getString('audience');
const baseURL = audience || 'https://gitlab.com';
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
for (const [env, envConfig] of Object.entries(providerConfig)) {
const {
secure,
appOrigin,
clientId,
clientSecret,
audience,
} = (envConfig as unknown) as OAuthProviderConfig;
const opts = {
clientID: clientId,
clientSecret: clientSecret,
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
baseURL: audience,
};
const opts = {
clientID,
clientSecret,
callbackURL,
baseURL,
};
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars',
);
}
logger.warn(
'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable',
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars',
);
continue;
}
envProviders[env] = new OAuthProvider(new GitlabAuthProvider(opts), {
disableRefresh: true,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
logger.warn(
'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable',
);
return undefined;
}
return new EnvironmentHandler(providerId, envProviders);
return new OAuthProvider(new GitlabAuthProvider(opts), {
disableRefresh: true,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
}
@@ -27,20 +27,15 @@ import {
OAuthProviderHandlers,
RedirectInfo,
AuthProviderConfig,
EnvironmentProviderConfig,
OAuthProviderOptions,
OAuthProviderConfig,
OAuthResponse,
PassportDoneCallback,
} from '../types';
import { OAuthProvider } from '../../lib/OAuthProvider';
import passport from 'passport';
import {
EnvironmentHandler,
EnvironmentHandlers,
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import { Config } from '@backstage/config';
type PrivateInfo = {
refreshToken: string;
@@ -150,43 +145,42 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
export function createGoogleProvider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
env: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'google';
const envProviders: EnvironmentHandlers = {};
const secure = envConfig.getBoolean('secure');
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as OAuthProviderConfig;
const { secure, appOrigin } = config;
const opts = {
clientID: config.clientId,
clientSecret: config.clientSecret,
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
};
const opts = {
clientID,
clientSecret,
callbackURL,
};
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars',
);
}
logger.warn(
'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable',
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars',
);
continue;
}
envProviders[env] = new OAuthProvider(new GoogleAuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
logger.warn(
'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable',
);
return undefined;
}
return new EnvironmentHandler(providerId, envProviders);
return new OAuthProvider(new GoogleAuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
}
@@ -19,10 +19,6 @@ import passport from 'passport';
import { Strategy as OAuth2Strategy } from 'passport-oauth2';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import {
EnvironmentHandler,
EnvironmentHandlers,
} from '../../lib/EnvironmentHandler';
import { OAuthProvider } from '../../lib/OAuthProvider';
import {
executeFetchUserProfileStrategy,
@@ -33,14 +29,13 @@ import {
} from '../../lib/PassportStrategyHelper';
import {
AuthProviderConfig,
EnvironmentProviderConfig,
GenericOAuth2ProviderConfig,
GenericOAuth2ProviderOptions,
OAuthProviderHandlers,
OAuthResponse,
PassportDoneCallback,
RedirectInfo,
} from '../types';
import { Config } from '@backstage/config';
type PrivateInfo = {
refreshToken: string;
@@ -148,51 +143,51 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers {
export function createOAuth2Provider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
env: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'oauth2';
const envProviders: EnvironmentHandlers = {};
const secure = envConfig.getBoolean('secure');
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
const authorizationURL = envConfig.getString('authorizationURL');
const tokenURL = envConfig.getString('tokenURL');
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as GenericOAuth2ProviderConfig;
const { secure, appOrigin } = config;
const opts = {
clientID: config.clientId,
clientSecret: config.clientSecret,
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
authorizationURL: config.authorizationURL,
tokenURL: config.tokenURL,
};
const opts = {
clientID,
clientSecret,
callbackURL,
authorizationURL,
tokenURL,
};
if (
!opts.clientID ||
!opts.clientSecret ||
!opts.authorizationURL ||
!opts.tokenURL
) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars',
);
}
logger.warn(
'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable',
if (
!opts.clientID ||
!opts.clientSecret ||
!opts.authorizationURL ||
!opts.tokenURL
) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars',
);
continue;
}
envProviders[env] = new OAuthProvider(new OAuth2AuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
logger.warn(
'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable',
);
return undefined;
}
return new EnvironmentHandler(providerId, envProviders);
return new OAuthProvider(new OAuth2AuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
}
@@ -28,19 +28,14 @@ import {
OAuthProviderHandlers,
RedirectInfo,
AuthProviderConfig,
EnvironmentProviderConfig,
OAuthProviderOptions,
OAuthProviderConfig,
OAuthResponse,
PassportDoneCallback,
} from '../types';
import {
EnvironmentHandler,
EnvironmentHandlers,
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { StateStore } from 'passport-oauth2';
import { TokenIssuer } from '../../identity';
import { Config } from '@backstage/config';
type PrivateInfo = {
refreshToken: string;
@@ -170,45 +165,44 @@ export class OktaAuthProvider implements OAuthProviderHandlers {
export function createOktaProvider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
env: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'okta';
const envProviders: EnvironmentHandlers = {};
const secure = envConfig.getBoolean('secure');
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const audience = envConfig.getString('audience');
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as OAuthProviderConfig;
const { secure, appOrigin } = config;
const opts = {
audience: config.audience,
clientID: config.clientId,
clientSecret: config.clientSecret,
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
};
const opts = {
audience,
clientID,
clientSecret,
callbackURL,
};
if (!opts.clientID || !opts.clientSecret || !opts.audience) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars',
);
}
logger.warn(
'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable',
if (!opts.clientID || !opts.clientSecret || !opts.audience) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars',
);
continue;
}
envProviders[env] = new OAuthProvider(new OktaAuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
logger.warn(
'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable',
);
return undefined;
}
return new EnvironmentHandler(providerId, envProviders);
return new OAuthProvider(new OktaAuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
}
@@ -27,18 +27,13 @@ import {
import {
AuthProviderConfig,
AuthProviderRouteHandlers,
EnvironmentProviderConfig,
SAMLProviderConfig,
PassportDoneCallback,
ProfileInfo,
} from '../types';
import { postMessageResponse } from '../../lib/OAuthProvider';
import {
EnvironmentHandlers,
EnvironmentHandler,
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import { Config } from '@backstage/config';
type SamlInfo = {
userId: string;
@@ -122,30 +117,25 @@ type SAMLProviderOptions = {
export function createSamlProvider(
_authProviderConfig: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
_env: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const envProviders: EnvironmentHandlers = {};
const entryPoint = envConfig.getString('entryPoint');
const issuer = envConfig.getString('issuer');
const opts = {
entryPoint,
issuer,
path: '/auth/saml/handler/frame',
tokenIssuer,
};
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as SAMLProviderConfig;
const opts = {
entryPoint: config.entryPoint,
issuer: config.issuer,
path: '/auth/saml/handler/frame',
tokenIssuer,
};
if (!opts.entryPoint || !opts.issuer) {
logger.warn(
'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable',
);
continue;
}
envProviders[env] = new SamlAuthProvider(opts);
if (!opts.entryPoint || !opts.issuer) {
logger.warn(
'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable',
);
return undefined;
}
return new EnvironmentHandler('saml', envProviders);
return new SamlAuthProvider(opts);
}
+6 -2
View File
@@ -17,6 +17,9 @@
import express from 'express';
import { Logger } from 'winston';
import { TokenIssuer } from '../identity';
import { Config } from '@backstage/config';
import { OAuthProvider } from '../lib/OAuthProvider';
import { SamlAuthProvider } from './saml/provider';
export type OAuthProviderOptions = {
/**
@@ -204,10 +207,11 @@ export interface AuthProviderRouteHandlers {
export type AuthProviderFactory = (
globalConfig: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
env: string,
envConfig: Config,
logger: Logger,
issuer: TokenIssuer,
) => AuthProviderRouteHandlers;
) => OAuthProvider | SamlAuthProvider | undefined;
export type AuthResponse<ProviderInfo> = {
providerInfo: ProviderInfo;

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