Merge pull request #33421 from awanlin/docs/clean-up-items
Clean up of the contrib folder
This commit is contained in:
@@ -36,7 +36,7 @@ The changes to the service-to-service auth are aimed to be the minimum needed to
|
||||
|
||||
## Motivation
|
||||
|
||||
This proposal aims to address several of the points in the [Auth Meta issue](https://github.com/backstage/backstage/issues/15999), with the overarching goal being to replace the existing [API request authentication](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/authenticate-api-requests.md) tutorial in `contrib/` with a more robust and secure built-in solution. The tutorial exists for two purposes: to add authentication of API requests as part of using the permission system in Backstage, and to protect a Backstage instance from external access. It does a fairly good job of the former, although we want to avoid placing user tokens in cookies, but it does a quite poor job of the latter, which we want to fix.
|
||||
This proposal aims to address several of the points in the [Auth Meta issue](https://github.com/backstage/backstage/issues/15999), with the overarching goal being to replace the existing [API request authentication](https://github.com/backstage/backstage/blob/a93b7fdafb6789277661ba77b8cc3390de82db27/contrib/docs/tutorials/authenticate-api-requests.md) tutorial in `contrib/` with a more robust and secure built-in solution. The tutorial exists for two purposes: to add authentication of API requests as part of using the permission system in Backstage, and to protect a Backstage instance from external access. It does a fairly good job of the former, although we want to avoid placing user tokens in cookies, but it does a quite poor job of the latter, which we want to fix.
|
||||
|
||||
A secondary goal is to do this work before stabilizing the APIs in the new Backend system, as it will have some impact on how plugin backends are built. This will inevitably also lead to the need to improve the way that service-to-service auth is handled in Backstage, although that is not the primary goal of this work.
|
||||
|
||||
|
||||
@@ -1,459 +0,0 @@
|
||||
# Authenticate API requests
|
||||
|
||||
> [!CAUTION]
|
||||
> This entire guide MUST NOT BE USED by users of Backstage 1.26 and
|
||||
> newer. If you have applied the changes in this guide, you need to remove them
|
||||
> again as you upgrade to recent versions of Backstage. When [the new auth changes](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution)
|
||||
> landed backends became natively secured through the framework, and the
|
||||
> instructions outlined in here can interfere with the backend functioning
|
||||
> correctly.
|
||||
|
||||
The Backstage backend APIs are by default available without authentication. To avoid evil-doers from accessing or modifying data, one might use a network protection mechanism such as a firewall or an authenticating reverse proxy. For Backstage instances that are available on the Internet one can instead use the experimental IdentityClient as outlined below.
|
||||
|
||||
API requests from frontend plugins include an authorization header with a Backstage identity token acquired when the user logs in. By adding a middleware that verifies said token to be valid and signed by Backstage, non-authenticated requests can be blocked with a 401 Unauthorized response.
|
||||
|
||||
**NOTE**: Enabling this means that Backstage will stop working for guests, as no token is issued for them. If you have not done so already, you will also need to implement [service-to-service auth](https://backstage.io/docs/auth/service-to-service-auth).
|
||||
|
||||
As techdocs HTML pages load assets without an Authorization header the code below also sets a token cookie when the user logs in (and when the token is about to expire).
|
||||
|
||||
## Old Backend System Setup
|
||||
|
||||
Create `packages/backend/src/authMiddleware.ts`:
|
||||
|
||||
```typescript
|
||||
import type { Config } from '@backstage/config';
|
||||
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
|
||||
import { NextFunction, Request, Response, RequestHandler } from 'express';
|
||||
import { decodeJwt } from 'jose';
|
||||
import { URL } from 'url';
|
||||
import { PluginEnvironment } from './types';
|
||||
|
||||
function setTokenCookie(
|
||||
res: Response,
|
||||
options: { token: string; secure: boolean; cookieDomain: string },
|
||||
) {
|
||||
try {
|
||||
const payload = decodeJwt(options.token);
|
||||
res.cookie('token', options.token, {
|
||||
expires: new Date(payload.exp ? payload.exp * 1000 : 0),
|
||||
secure: options.secure,
|
||||
sameSite: 'lax',
|
||||
domain: options.cookieDomain,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
});
|
||||
} catch (_err) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
export const createAuthMiddleware = async (
|
||||
config: Config,
|
||||
appEnv: PluginEnvironment,
|
||||
) => {
|
||||
const baseUrl = config.getString('backend.baseUrl');
|
||||
const secure = baseUrl.startsWith('https://');
|
||||
const cookieDomain = new URL(baseUrl).hostname;
|
||||
const authMiddleware: RequestHandler = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
const token =
|
||||
getBearerTokenFromAuthorizationHeader(req.headers.authorization) ||
|
||||
(req.cookies?.token as string | undefined);
|
||||
if (!token) {
|
||||
res.status(401).send('Unauthorized');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
req.user = await appEnv.identity.getIdentity({ request: req });
|
||||
} catch {
|
||||
await appEnv.tokenManager.authenticate(token);
|
||||
}
|
||||
if (!req.headers.authorization) {
|
||||
// Authorization header may be forwarded by plugin requests
|
||||
req.headers.authorization = `Bearer ${token}`;
|
||||
}
|
||||
if (token && token !== req.cookies?.token) {
|
||||
setTokenCookie(res, {
|
||||
token,
|
||||
secure,
|
||||
cookieDomain,
|
||||
});
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).send('Unauthorized');
|
||||
}
|
||||
};
|
||||
return authMiddleware;
|
||||
};
|
||||
```
|
||||
|
||||
Install cookie-parser:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn --cwd packages/backend add cookie-parser
|
||||
```
|
||||
|
||||
Update routes in `packages/backend/src/index.ts`:
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/index.ts from a create-app deployment
|
||||
|
||||
import { createAuthMiddleware } from './authMiddleware';
|
||||
import cookieParser from 'cookie-parser';
|
||||
|
||||
// ...
|
||||
|
||||
async function main() {
|
||||
// ...
|
||||
|
||||
const authMiddleware = await createAuthMiddleware(config, appEnv);
|
||||
|
||||
const apiRouter = Router();
|
||||
apiRouter.use(cookieParser());
|
||||
// The auth route must be publicly available as it is used during login
|
||||
apiRouter.use('/auth', await auth(authEnv));
|
||||
// Add a simple endpoint to be used when setting a token cookie
|
||||
apiRouter.use('/cookie', authMiddleware, (_req, res) => {
|
||||
res.status(200).send(`Coming right up`);
|
||||
});
|
||||
// Only authenticated requests are allowed to the routes below
|
||||
apiRouter.use('/catalog', authMiddleware, await catalog(catalogEnv));
|
||||
apiRouter.use('/techdocs', authMiddleware, await techdocs(techdocsEnv));
|
||||
apiRouter.use('/proxy', authMiddleware, await proxy(proxyEnv));
|
||||
apiRouter.use(authMiddleware, notFoundHandler());
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## New Backend System Setup
|
||||
|
||||
Create `packages/backend/src/authMiddlewareFactory.ts`:
|
||||
|
||||
```typescript
|
||||
import { HostDiscovery } from '@backstage/backend-app-api';
|
||||
import { ServerTokenManager } from '@backstage/backend-common';
|
||||
import {
|
||||
LoggerService,
|
||||
RootConfigService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
DefaultIdentityClient,
|
||||
getBearerTokenFromAuthorizationHeader,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { NextFunction, Request, RequestHandler, Response } from 'express';
|
||||
import { decodeJwt } from 'jose';
|
||||
import lzstring from 'lz-string';
|
||||
import { URL } from 'url';
|
||||
|
||||
type AuthMiddlewareFactoryOptions = {
|
||||
config: RootConfigService;
|
||||
logger: LoggerService;
|
||||
};
|
||||
|
||||
export const authMiddlewareFactory = ({
|
||||
config,
|
||||
logger,
|
||||
}: AuthMiddlewareFactoryOptions): RequestHandler => {
|
||||
const baseUrl = config.getString('backend.baseUrl');
|
||||
const discovery = HostDiscovery.fromConfig(config);
|
||||
const identity = DefaultIdentityClient.create({ discovery });
|
||||
const tokenManager = ServerTokenManager.fromConfig(config, { logger });
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const fullPath = `${req.baseUrl}${req.path}`;
|
||||
|
||||
// Only apply auth to /api routes & skip auth for the following endpoints
|
||||
// Add any additional plugin routes you want to whitelist eg. events
|
||||
const nonAuthWhitelist = ['app', 'auth'];
|
||||
const nonAuthRegex = new RegExp(
|
||||
`^\/api\/(${nonAuthWhitelist.join('|')})(?=\/|$)\S*`,
|
||||
);
|
||||
if (!fullPath.startsWith('/api/') || nonAuthRegex.test(fullPath)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Token cookies are compressed to reduce size
|
||||
const cookieToken = lzstring.decompressFromEncodedURIComponent(
|
||||
req.cookies.token,
|
||||
);
|
||||
const token =
|
||||
getBearerTokenFromAuthorizationHeader(req.headers.authorization) ??
|
||||
cookieToken;
|
||||
|
||||
try {
|
||||
// Attempt to authenticate as a frontend request token
|
||||
await identity.authenticate(token);
|
||||
} catch (err) {
|
||||
// Attempt to authenticate as a backend request token
|
||||
await tokenManager.authenticate(token);
|
||||
}
|
||||
|
||||
if (!req.headers.authorization) {
|
||||
// Authorization header may be forwarded by plugin requests
|
||||
req.headers.authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (token !== cookieToken) {
|
||||
try {
|
||||
const payload = decodeJwt(token);
|
||||
res.cookie('token', token, {
|
||||
// Compress token to reduce cookie size
|
||||
encode: lzstring.compressToEncodedURIComponent,
|
||||
expires: new Date((payload?.exp ?? 0) * 1000),
|
||||
secure: baseUrl.startsWith('https://'),
|
||||
sameSite: 'lax',
|
||||
domain: new URL(baseUrl).hostname,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
});
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).send(`Unauthorized`);
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Install cookie-parser:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn --cwd packages/backend add cookie-parser @types/cookie-parser
|
||||
```
|
||||
|
||||
Create a custom configured `rootHttpRouterService` in `packages/backend/src/customRootHttpRouterService.ts`:
|
||||
|
||||
```typescript
|
||||
import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { authMiddlewareFactory } from './authMiddlewareFactory';
|
||||
|
||||
export default rootHttpRouterServiceFactory({
|
||||
configure: ({ app, config, logger, middleware, routes }) => {
|
||||
app.use(middleware.helmet());
|
||||
app.use(middleware.cors());
|
||||
app.use(middleware.compression());
|
||||
app.use(cookieParser());
|
||||
app.use(middleware.logging());
|
||||
|
||||
app.use(authMiddlewareFactory({ config, logger }));
|
||||
|
||||
// Simple handler to set auth cookie for user
|
||||
app.use('/api/cookie', (_, res) => {
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.use(routes);
|
||||
|
||||
app.use(middleware.notFound());
|
||||
app.use(middleware.error());
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Update `packages/backend/src/index.ts` to add the custom `rootHttpRouterService` and override the default:
|
||||
|
||||
```typescript
|
||||
// ...
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('./customRootHttpRouterService'));
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
## Frontend Setup
|
||||
|
||||
Create `packages/app/src/cookieAuth.ts`:
|
||||
|
||||
```typescript
|
||||
import type { IdentityApi } from '@backstage/core-plugin-api';
|
||||
|
||||
// Parses supplied JWT token and returns the payload
|
||||
function parseJwt(token: string): { exp: number } {
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map(
|
||||
c =>
|
||||
// eslint-disable-next-line prefer-template
|
||||
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2),
|
||||
)
|
||||
.join(''),
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload);
|
||||
}
|
||||
|
||||
// Returns milliseconds until the supplied JWT token expires
|
||||
function msUntilExpiry(token: string): number {
|
||||
const payload = parseJwt(token);
|
||||
const remaining =
|
||||
new Date(payload.exp * 1000).getTime() - new Date().getTime();
|
||||
return remaining;
|
||||
}
|
||||
|
||||
// Calls the specified url regularly using an auth token to set a token cookie
|
||||
// to authorize regular HTTP requests when loading techdocs
|
||||
export async function setTokenCookie(url: string, identityApi: IdentityApi) {
|
||||
const { token } = await identityApi.getCredentials();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch(url, {
|
||||
mode: 'cors',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Call this function again a few minutes before the token expires
|
||||
const ms = msUntilExpiry(token) - 4 * 60 * 1000;
|
||||
setTimeout(
|
||||
() => {
|
||||
setTokenCookie(url, identityApi);
|
||||
},
|
||||
ms > 0 ? ms : 10000,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// required types and packages for example below
|
||||
|
||||
import type { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
// additional packages/app/src/App.tsx from a create-app deployment
|
||||
|
||||
import { setTokenCookie } from './cookieAuth';
|
||||
|
||||
// ...
|
||||
|
||||
const app = createApp({
|
||||
// ...
|
||||
|
||||
components: {
|
||||
SignInPage: props => {
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
return (
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={['guest', 'custom', ...providers]}
|
||||
title="Select a sign-in method"
|
||||
align="center"
|
||||
onSignInSuccess={async (identityApi: IdentityApi) => {
|
||||
setTokenCookie(
|
||||
await discoveryApi.getBaseUrl('cookie'),
|
||||
identityApi,
|
||||
);
|
||||
|
||||
props.onSignInSuccess(identityApi);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// ...
|
||||
});
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
**NOTE**: Most Backstage frontend plugins come with the support for the `IdentityApi`.
|
||||
In case you already have a dozen of internal ones, you may need to update those too.
|
||||
Assuming you follow the common plugin structure, the changes to your front-end may look like:
|
||||
|
||||
```diff
|
||||
// plugins/internal-plugin/src/api.ts
|
||||
- import { createApiRef } from '@backstage/core-plugin-api';
|
||||
+ import { createApiRef, IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
// ...
|
||||
|
||||
type MyApiOptions = {
|
||||
configApi: Config;
|
||||
+ identityApi: IdentityApi;
|
||||
// ...
|
||||
}
|
||||
|
||||
interface MyInterface {
|
||||
getData(): Promise<MyData[]>;
|
||||
}
|
||||
|
||||
export class MyApi implements MyInterface {
|
||||
private configApi: Config;
|
||||
+ private identityApi: IdentityApi;
|
||||
// ...
|
||||
|
||||
constructor(options: MyApiOptions) {
|
||||
this.configApi = options.configApi;
|
||||
+ this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
async getMyData() {
|
||||
const backendUrl = this.configApi.getString('backend.baseUrl');
|
||||
|
||||
+ const { token } = await this.identityApi.getCredentials();
|
||||
const requestUrl = `${backendUrl}/api/data/`;
|
||||
- const response = await fetch(requestUrl);
|
||||
+ const response = await fetch(
|
||||
requestUrl,
|
||||
{ headers: { Authorization: `Bearer ${token}` } },
|
||||
);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```diff
|
||||
// plugins/internal-plugin/src/plugin.ts
|
||||
|
||||
import {
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
+ identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { myPluginPageRouteRef } from './routeRefs';
|
||||
import { MyApi, myApiRef } from './api';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'my-plugin',
|
||||
routes: {
|
||||
mainPage: myPluginPageRouteRef,
|
||||
},
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: myApiRef,
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
+ identityApi: identityApiRef,
|
||||
},
|
||||
- factory: ({ configApi }) =>
|
||||
- new MyApi({ configApi }),
|
||||
+ factory: ({ configApi, identityApi }) =>
|
||||
+ new MyApi({ configApi, identityApi }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 252 KiB |
@@ -1,115 +0,0 @@
|
||||
# Prometheus
|
||||
|
||||
> [!NOTE]
|
||||
> The Prometheus metrics have been marked as deprecated and will be removed at a later point. The recommendation is to use the OpenTelemetry metrics by following the [Setup OpenTelemetry](https://backstage.io/docs/tutorials/setup-opentelemetry) documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This is a small tutorial that goes over how to setup your Backstage instance to output metrics in a format that can be pulled in by Prometheus.
|
||||
|
||||
## How to Setup Prometheus Metrics
|
||||
|
||||
1. First we need to add the needed dependencies to the `package.json` in the `\packages\backend`:
|
||||
|
||||
```diff
|
||||
// packages/backend/package.json
|
||||
"dependencies": {
|
||||
+ "express-prom-bundle": "^7.0.0",
|
||||
+ "prom-client": "^15.0.0",
|
||||
```
|
||||
|
||||
2. Now we want to run `yarn install` from the root of the project to get those dependencies in place
|
||||
3. Then we need to add a handler for the metrics by creating a file called `metrics.ts` in the `\packages\backend\src` folder
|
||||
4. Next we add the following content to the `metrics.ts` file:
|
||||
|
||||
```ts
|
||||
// packages/backend/src/metrics.ts
|
||||
import { useHotCleanup } from '@backstage/backend-common';
|
||||
import { RequestHandler } from 'express';
|
||||
import promBundle from 'express-prom-bundle';
|
||||
import prom from 'prom-client';
|
||||
import * as url from 'url';
|
||||
|
||||
const rootRegEx = new RegExp('^/([^/]*)/.*');
|
||||
const apiRegEx = new RegExp('^/api/([^/]*)/.*');
|
||||
|
||||
export function normalizePath(req: any): string {
|
||||
const path = url.parse(req.originalUrl || req.url).pathname || '/';
|
||||
|
||||
// Capture /api/ and the plugin name
|
||||
if (apiRegEx.test(path)) {
|
||||
return path.replace(apiRegEx, '/api/$1');
|
||||
}
|
||||
|
||||
// Only the first path segment at root level
|
||||
return path.replace(rootRegEx, '/$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a /metrics endpoint, register default runtime metrics and instrument the router.
|
||||
*/
|
||||
export function metricsHandler(): RequestHandler {
|
||||
// We can only initialize the metrics once and have to clean them up between hot reloads
|
||||
useHotCleanup(module, () => prom.register.clear());
|
||||
|
||||
return promBundle({
|
||||
includeMethod: true,
|
||||
includePath: true,
|
||||
// Using includePath alone is problematic, as it will include path labels with high
|
||||
// cardinality (e.g. path params). Instead we would have to template them. However, this
|
||||
// is difficult, as every backend plugin might use different routes. Instead we only take
|
||||
// the first directory of the path, to have at least an idea how each plugin performs:
|
||||
normalizePath,
|
||||
promClient: { collectDefaultMetrics: {} },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
5. Now we will extend the router configuration with the `metricsHandler`:
|
||||
|
||||
```diff
|
||||
+import { metricsHandler } from './metrics';
|
||||
|
||||
...
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.loadConfig(config)
|
||||
.addRouter('', await healthcheck(healthcheckEnv))
|
||||
+ .addRouter('', metricsHandler())
|
||||
.addRouter('/api', apiRouter);
|
||||
```
|
||||
|
||||
6. You now have everything setup, from the `\packages\backend` folder run `yarn start` this will start up the backend
|
||||
7. Now in a browser load up `http://localhost:7007/metrics`, if everything went smoothly you should see metrics in your browser something like this:
|
||||
|
||||

|
||||
|
||||
## Metrics
|
||||
|
||||
The following sections goes over the included and experimental metrics available once you have completed this tutorial
|
||||
|
||||
## Included
|
||||
|
||||
This tutorials uses the [`express-prom-bundle`](https://github.com/jochen-schweizer/express-prom-bundle) and the [`prom-client`](https://github.com/siimon/prom-client) to make this all work. They both come with some built in metrics:
|
||||
|
||||
- `express-prom-bundle` comes with 2 metrics:
|
||||
- `up`: this normally will be just 1
|
||||
- `http_request_duration_seconds`: http latency histogram/summary labeled with `status_code`, `method` and `path`
|
||||
- `prom-client` comes with a collection of metrics around memory, CPU, processes, etc. You can see the supported metrics in the `prom-client's` [`lib/metrics`](https://github.com/siimon/prom-client/tree/master/lib/metrics) folder.
|
||||
|
||||
### Experimental
|
||||
|
||||
There are some custom metrics that have been added to Backstage will be output for you, these are currently deemed experimental and may be changed or removed in a future release. Here is a rough list, again subject to changes:
|
||||
|
||||
- `catalog_entities_count`: Total amount of entities in the catalog
|
||||
- `catalog_registered_locations_count`: Total amount of registered locations in the catalog
|
||||
- `catalog_relations_count`: Total amount of relations between entities
|
||||
- `catalog_stitched_entities_count`: Amount of entities stitched
|
||||
- `catalog_processed_entities_count`: Amount of entities processed
|
||||
- `catalog_processing_duration_seconds`: Time spent executing the full processing flow
|
||||
- `catalog_processors_duration_seconds`: Time spent executing catalog processors
|
||||
- `catalog_processing_queue_delay_seconds`: The amount of delay between being scheduled for processing, and the start of actually being processed
|
||||
- `scaffolder_task_count`: Tracks successful task runs.
|
||||
- `scaffolder_task_duration`: a histogram which tracks the duration of a task run
|
||||
- `scaffolder_step_count`: a count that tracks each step run
|
||||
- `scaffolder_step_duration`: a histogram which tracks the duration of each step run
|
||||
@@ -1,53 +0,0 @@
|
||||
### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin
|
||||
|
||||
ExampleComponent.tsx reference
|
||||
|
||||
```tsx
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
InfoCard,
|
||||
Header,
|
||||
Page,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
import { ExampleFetchComponent } from '../ExampleFetchComponent';
|
||||
|
||||
export const ExampleComponent = () => {
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const userId = identityApi.getUserId();
|
||||
const profile = identityApi.getProfile();
|
||||
|
||||
return (
|
||||
<Page themeId="tool">
|
||||
<Header
|
||||
title="Welcome to github-playground!"
|
||||
subtitle="Optional subtitle"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Plugin title">
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard title={userId}>
|
||||
<Typography variant="body1">
|
||||
{`${profile.displayName} | ${profile.email}`}
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ExampleFetchComponent />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -1,103 +0,0 @@
|
||||
### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin
|
||||
|
||||
ExampleFetchComponent.tsx reference
|
||||
|
||||
```tsx
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { githubAuthApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { Table, TableColumn, Progress } from '@backstage/core-components';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
|
||||
const query = `{
|
||||
viewer {
|
||||
repositories(first: 100) {
|
||||
totalCount
|
||||
nodes {
|
||||
name
|
||||
createdAt
|
||||
description
|
||||
diskUsage
|
||||
isFork
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
type Node = {
|
||||
name: string;
|
||||
createdAt: string;
|
||||
description: string;
|
||||
diskUsage: number;
|
||||
isFork: boolean;
|
||||
};
|
||||
|
||||
type Viewer = {
|
||||
repositories: {
|
||||
totalCount: number;
|
||||
nodes: Node[];
|
||||
pageInfo: {
|
||||
endCursor: string;
|
||||
hasNextPage: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type DenseTableProps = {
|
||||
viewer: Viewer;
|
||||
};
|
||||
|
||||
export const DenseTable = ({ viewer }: DenseTableProps) => {
|
||||
const columns: TableColumn[] = [
|
||||
{ title: 'Name', field: 'name' },
|
||||
{ title: 'Created', field: 'createdAt' },
|
||||
{ title: 'Description', field: 'description' },
|
||||
{ title: 'Disk Usage', field: 'diskUsage' },
|
||||
{ title: 'Fork', field: 'isFork' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={viewer.repositories.nodes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExampleFetchComponent = () => {
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
|
||||
const { value, loading, error } = useAsync(async (): Promise<any> => {
|
||||
const token = await auth.getAccessToken();
|
||||
|
||||
const gqlEndpoint = graphql.defaults({
|
||||
// Uncomment baseUrl if using enterprise
|
||||
// baseUrl: 'https://github.MY-BIZ.com/api',
|
||||
headers: {
|
||||
authorization: `token ${token}`,
|
||||
},
|
||||
});
|
||||
const { viewer } = await gqlEndpoint(query);
|
||||
return viewer;
|
||||
}, []);
|
||||
|
||||
if (loading) return <Progress />;
|
||||
if (error) return <Alert severity="error">{error.message}</Alert>;
|
||||
if (value && value.repositories) return <DenseTable viewer={value} />;
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={[]}
|
||||
data={[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -1,3 +1,6 @@
|
||||
# Basic Kubernetes example with Helm
|
||||
|
||||
> [!NOTE]
|
||||
> This documentation is deprecated and will be removed at a future date, please use the well-maintained [Backstage Helm Charts](https://github.com/backstage/charts) for this.
|
||||
|
||||
Note that these examples aim to show a minimal setup and do not include best practices for secure Kubernetes deployments. See the [Kubernetes documentation](https://kubernetes.io/docs/concepts/security/) for more information, or resources provided by your own organization.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Confluence
|
||||
|
||||
> [!NOTE]
|
||||
> This documentation is deprecated and will be removed at a future date. Please use the well-maintained [`@backstage-community/plugin-search-backend-module-confluence-collator` Community Plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/confluence/plugins/search-backend-module-confluence-collator) instead.
|
||||
|
||||
These files help you add Confluence as a source to the Backstage Search plugin.
|
||||
To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app.
|
||||
Then, add the following code to your packages/app/src/components/search/SearchPage.tsx:
|
||||
|
||||
@@ -646,7 +646,7 @@
|
||||
- `step`: The name of the step that was run
|
||||
- `result`: A string describing whether the task ran successfully, failed, or was skipped
|
||||
|
||||
You can find a guide for running Prometheus metrics here: <https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/prometheus-metrics.md>
|
||||
You can find a guide for running Prometheus metrics here: <https://github.com/backstage/backstage/blob/384b7bac2e/contrib/docs/tutorials/prometheus-metrics.md>
|
||||
|
||||
- 5921b5ce49: - The GitLab Project ID for the `publish:gitlab:merge-request` action is now passed through the query parameter `project` in the `repoUrl`. It still allows people to not use the `projectid` and use the `repoUrl` with the `owner` and `repo` query parameters instead. This makes it easier to publish to repositories instead of writing the full path to the project.
|
||||
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "backstage.io/v1alpha1",
|
||||
"type": "object",
|
||||
"title": "A JSON Schema for Backstage catalog entities.",
|
||||
"description": "Each descriptor file has a number of entities. This schema matches each of those.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Component",
|
||||
"metadata": {
|
||||
"name": "LoremService",
|
||||
"description": "Creates Lorems like a pro.",
|
||||
"labels": {
|
||||
"product_name": "Random value Generator"
|
||||
},
|
||||
"annnotations": {
|
||||
"docs": "https://github.com/..../tree/develop/doc"
|
||||
},
|
||||
"teams": [
|
||||
{
|
||||
"name": "Team super great",
|
||||
"email": "greatTeam@geemel.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spec": {
|
||||
"type": "service",
|
||||
"lifecycle": "production",
|
||||
"owner": "tools@example.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"required": ["apiVersion", "kind", "metadata"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"type": "string",
|
||||
"description": "Version of the specification format for a particular file is written against.",
|
||||
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "High level entity type being described, from the Backstage system model.",
|
||||
"enum": ["Component"]
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "#/definitions/metadata"
|
||||
},
|
||||
"spec": {
|
||||
"$ref": "#/definitions/spec"
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Metadata about the entity, i.e. things that aren't directly part of the entity specification itself.",
|
||||
"required": ["name"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9A-Z_.-]{1,63}$",
|
||||
"description": "The name of the entity. This name is both meant for human eyes to recognize the entity, and for machines and other components to reference the entity"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A human readable description of the entity, to be shown in Backstage. Should be kept short and informative."
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "The name of a namespace that the entity belongs to."
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"description": "Labels are optional key/value pairs of that are attached to the entity, and their use is identical to kubernetes object labels.",
|
||||
"additionalProperties": true,
|
||||
"patternProperties": {
|
||||
"^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}/)?[a-z0-9A-Z_\\-\\.]{1,63}$": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9A-Z_.-]{1,63}$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"annnotations": {
|
||||
"type": "object",
|
||||
"description": "Arbitrary non-identifying metadata attached to the entity, identical in use to kubernetes object annotations.",
|
||||
"additionalProperties": true,
|
||||
"patternProperties": {
|
||||
"^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}/)?[a-z0-9A-Z_\\-\\.]{1,63}$": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9A-Z_.-]{1,63}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"description": "Actual specification data that describes the entity. TODO: shape depend on `kind`",
|
||||
"required": ["type", "lifecycle", "owner"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The type of component.",
|
||||
"examples": ["service"]
|
||||
},
|
||||
"lifecycle": {
|
||||
"type": "string",
|
||||
"description": "The lifecycle step that this component is in.",
|
||||
"examples": ["production"]
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "The owner of the component.",
|
||||
"examples": ["tools@example.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
---
|
||||
id: quickstart-app-plugin--old
|
||||
title: Adding Custom Plugin to Existing Monorepo App (Old Frontend System)
|
||||
description: Tutorial for adding a custom plugin to an existing Backstage monorepo application
|
||||
---
|
||||
|
||||
::::info
|
||||
This documentation is for Backstage apps that still use the old frontend
|
||||
system. If your app uses the new frontend system, read the
|
||||
[current guide](./quickstart-app-plugin.md) instead.
|
||||
::::
|
||||
|
||||
###### September 15th 2020 - v0.1.1-alpha.21
|
||||
|
||||
<br />
|
||||
|
||||
> This document takes you through setting up a new plugin for your existing
|
||||
> monorepo with a _GitHub provider already setup_. If you don't have either of
|
||||
> those, you can clone
|
||||
> [simple-backstage-app](https://github.com/johnson-jesse/simple-backstage-app)
|
||||
> which this document builds on.
|
||||
>
|
||||
> This document does not cover authoring a plugin for sharing with the Backstage
|
||||
> community. That will have to be a later discussion.
|
||||
>
|
||||
> We start with a skeleton plugin install. And after verifying its
|
||||
> functionality, extend the Sidebar to make our life easy. Finally, we add
|
||||
> custom code to display GitHub repository information.
|
||||
>
|
||||
> This document assumes you have Node.js 16 active along with Yarn and Python.
|
||||
> Please note, that at the time of this writing, the current version is
|
||||
> 0.1.1-alpha.21. This guide can still be used with future versions, just,
|
||||
> verify as you go. If you run into issues, you can compare your setup with mine
|
||||
> here >
|
||||
> [simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin).
|
||||
|
||||
## The Skeleton Plugin
|
||||
|
||||
1. Start by using the built-in creator. From the terminal and root of your
|
||||
project run: `yarn new` and select `frontend-plugin`.
|
||||
1. Enter a plugin ID. I used `github-playground`
|
||||
1. When the process finishes, let's start the backend:
|
||||
`yarn --cwd packages/backend start`
|
||||
1. If you see errors starting, refer to
|
||||
[Auth Configuration](https://backstage.io/docs/auth/) for more information on
|
||||
environment variables.
|
||||
1. And now the frontend, from a new terminal window and the root of your
|
||||
project: `yarn start`
|
||||
1. As usual, a browser window should popup loading the App.
|
||||
1. Now manually navigate to our plugin page from your browser:
|
||||
`http://localhost:3000/github-playground`
|
||||
1. You should see successful verbiage for this endpoint,
|
||||
`Welcome to github-playground!`
|
||||
|
||||
## The Shortcut
|
||||
|
||||
Let's add a shortcut.
|
||||
|
||||
1. Open and modify `root: packages > app > src > components > Root.tsx` with the
|
||||
following:
|
||||
|
||||
```tsx
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
...
|
||||
<SidebarItem icon={GitHubIcon} to="github-playground" text="GitHub Repository" />
|
||||
```
|
||||
|
||||
Simple! The App will reload with your changes automatically. You should now see
|
||||
a GitHub icon displayed in the sidebar. Clicking that will link to our new
|
||||
plugin. And now, the API fun begins.
|
||||
|
||||
## The Identity
|
||||
|
||||
Our first modification will be to extract information from the Identity API.
|
||||
|
||||
1. Start by opening
|
||||
`root: plugins > github-playground > src > components > ExampleComponent > ExampleComponent.tsx`
|
||||
1. Add two new imports
|
||||
|
||||
```tsx
|
||||
// Add identityApiRef to the list of imported from core
|
||||
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
```
|
||||
|
||||
3. Adjust the ExampleComponent from inline to block
|
||||
|
||||
_from inline:_
|
||||
|
||||
```tsx
|
||||
const ExampleComponent = () => ( ... )
|
||||
```
|
||||
|
||||
_to block:_
|
||||
|
||||
```tsx
|
||||
const ExampleComponent = () => {
|
||||
|
||||
return (
|
||||
...
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
4. Now add our hook and const data before the return statement
|
||||
|
||||
```tsx
|
||||
// our API hook
|
||||
const identityApi = useApi(identityApiRef);
|
||||
|
||||
// data to use
|
||||
const userId = identityApi.getUserId();
|
||||
const profile = identityApi.getProfile();
|
||||
```
|
||||
|
||||
5. Finally, update the InfoCard's jsx to use our new data
|
||||
|
||||
```tsx
|
||||
<InfoCard title={userId}>
|
||||
<Typography variant="body1">
|
||||
{`${profile.displayName} | ${profile.email}`}
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
```
|
||||
|
||||
If everything is saved, you should see your name, id, and email on the
|
||||
github-playground page. Our data accessed is synchronous. So we just grab and
|
||||
go.
|
||||
|
||||
https://github.com/backstage/backstage/tree/master/contrib
|
||||
|
||||
6. Here is the entire file for reference
|
||||
[ExampleComponent.tsx](https://github.com/backstage/backstage/tree/master/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md)
|
||||
|
||||
## The Wipe
|
||||
|
||||
The last file we will touch is ExampleFetchComponent. Because of the number of
|
||||
changes, let's start by wiping this component clean.
|
||||
|
||||
1. Start by opening
|
||||
`root: plugins > github-playground > src > components > ExampleFetchComponent > ExampleFetchComponent.tsx`
|
||||
1. Replace everything in the file with the following:
|
||||
|
||||
```tsx
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { Table, TableColumn, Progress } from '@backstage/core-components';
|
||||
import { githubAuthApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
|
||||
export const ExampleFetchComponent = () => {
|
||||
return <div>Nothing to see yet</div>;
|
||||
};
|
||||
```
|
||||
|
||||
3. Save that and ensure you see no errors. Comment out the unused imports if
|
||||
your linter gets in the way.
|
||||
|
||||
###### We will add a lot to this file for the sake of ease. Please don't do this in production code!
|
||||
|
||||
## The Graph Model
|
||||
|
||||
GitHub has a GraphQL API available for interacting. Let's start by adding our
|
||||
basic repository query
|
||||
|
||||
1. Add the query const statement outside ExampleFetchComponent
|
||||
|
||||
```tsx
|
||||
const query = `{
|
||||
viewer {
|
||||
repositories(first: 100) {
|
||||
totalCount
|
||||
nodes {
|
||||
name
|
||||
createdAt
|
||||
description
|
||||
diskUsage
|
||||
isFork
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
```
|
||||
|
||||
2. Using this structure as a guide, we will break our query into type parts
|
||||
3. Add the following outside of ExampleFetchComponent
|
||||
|
||||
```tsx
|
||||
type Node = {
|
||||
name: string;
|
||||
createdAt: string;
|
||||
description: string;
|
||||
diskUsage: number;
|
||||
isFork: boolean;
|
||||
};
|
||||
|
||||
type Viewer = {
|
||||
repositories: {
|
||||
totalCount: number;
|
||||
nodes: Node[];
|
||||
pageInfo: {
|
||||
endCursor: string;
|
||||
hasNextPage: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## The Table Model
|
||||
|
||||
Using Backstage's own component library, let's define a custom table. This
|
||||
component will get used if we have data to display.
|
||||
|
||||
1. Add the following outside of ExampleFetchComponent
|
||||
|
||||
```tsx
|
||||
type DenseTableProps = {
|
||||
viewer: Viewer;
|
||||
};
|
||||
|
||||
export const DenseTable = ({ viewer }: DenseTableProps) => {
|
||||
const columns: TableColumn[] = [
|
||||
{ title: 'Name', field: 'name' },
|
||||
{ title: 'Created', field: 'createdAt' },
|
||||
{ title: 'Description', field: 'description' },
|
||||
{ title: 'Disk Usage', field: 'diskUsage' },
|
||||
{ title: 'Fork', field: 'isFork' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={viewer.repositories.nodes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## The Fetch
|
||||
|
||||
We're ready to flush out our fetch component
|
||||
|
||||
1. Add our api hook inside ExampleFetchComponent
|
||||
|
||||
```tsx
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
```
|
||||
|
||||
2. The access token we need to make our GitHub request and the request itself is
|
||||
obtained in an asynchronous manner.
|
||||
3. Add the `useAsync` block inside the ExampleFetchComponent
|
||||
|
||||
```tsx
|
||||
const { value, loading, error } = useAsync(async (): Promise<any> => {
|
||||
const token = await auth.getAccessToken();
|
||||
|
||||
const gqlEndpoint = graphql.defaults({
|
||||
// Uncomment baseUrl if using enterprise
|
||||
// baseUrl: 'https://github.MY-BIZ.com/api',
|
||||
headers: {
|
||||
authorization: `token ${token}`,
|
||||
},
|
||||
});
|
||||
const { viewer } = await gqlEndpoint(query);
|
||||
return viewer;
|
||||
}, []);
|
||||
```
|
||||
|
||||
4. The resolved data is conveniently destructured with `value` containing our
|
||||
Viewer type. `loading` as a boolean, self explanatory. And `error` which is
|
||||
present only if necessary. So let's use those as the first 3 of 4 multi
|
||||
return statements.
|
||||
5. Add the _if return_ blocks below our async block
|
||||
|
||||
```tsx
|
||||
if (loading) return <Progress />;
|
||||
if (error) return <Alert severity="error">{error.message}</Alert>;
|
||||
if (value && value.repositories) return <DenseTable viewer={value} />;
|
||||
```
|
||||
|
||||
6. The third line here utilizes our custom table accepting our Viewer type.
|
||||
7. Finally, we add our _else return_ block to catch any other scenarios.
|
||||
|
||||
```tsx
|
||||
return (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={[]}
|
||||
data={[]}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
8. After saving that, and given we don't have any errors, you should see a table
|
||||
with basic information on your repositories.
|
||||
9. Here is the entire file for reference
|
||||
[ExampleFetchComponent.tsx](https://github.com/backstage/backstage/tree/master/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md)
|
||||
10. We finished! You should see your own GitHub repository's information
|
||||
displayed in a basic table. If you run into issues, you can compare the repo
|
||||
that backs this document,
|
||||
[simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin)
|
||||
|
||||
## Where to go from here
|
||||
|
||||
> Break apart ExampleFetchComponent into smaller logical parts contained in
|
||||
> their own files. Rename your components to something other than ExampleXxx.
|
||||
>
|
||||
> You might be really proud of a plugin you develop. Consider sharing it with
|
||||
> the Backstage community by contributing to the [community-plugins repository](https://github.com/backstage/community-plugins).
|
||||
@@ -1,286 +0,0 @@
|
||||
---
|
||||
id: quickstart-app-plugin
|
||||
title: Adding Custom Plugin to Existing Monorepo App
|
||||
description: Tutorial for adding a custom plugin to an existing Backstage monorepo application
|
||||
---
|
||||
|
||||
::::info
|
||||
This documentation is written for the new frontend system, which is the default
|
||||
in new Backstage apps. If your Backstage app still uses the old frontend system,
|
||||
read the [old frontend system version of this guide](./quickstart-app-plugin--old.md)
|
||||
instead.
|
||||
::::
|
||||
|
||||
> This document takes you through setting up a new plugin for your existing
|
||||
> monorepo with a _GitHub provider already setup_.
|
||||
>
|
||||
> This document does not cover authoring a plugin for sharing with the Backstage
|
||||
> community. That will have to be a later discussion.
|
||||
>
|
||||
> We start with a skeleton plugin install. And after verifying its
|
||||
> functionality, we add custom code to display GitHub repository information.
|
||||
|
||||
## The Skeleton Plugin
|
||||
|
||||
1. Start by using the built-in creator. From the terminal and root of your
|
||||
project run: `yarn new` and select `frontend-plugin`.
|
||||
1. Enter a plugin ID. We'll use `github-playground` for this tutorial.
|
||||
1. When the process finishes, let's start the backend:
|
||||
`yarn --cwd packages/backend start`
|
||||
1. If you see errors starting, refer to
|
||||
[Auth Configuration](https://backstage.io/docs/auth/) for more information on
|
||||
environment variables.
|
||||
1. And now the frontend, from a new terminal window and the root of your
|
||||
project: `yarn start`
|
||||
1. As usual, a browser window should popup loading the App.
|
||||
1. Now manually navigate to the plugin page from your browser:
|
||||
`http://localhost:3000/github-playground`
|
||||
1. You should see successful verbiage for this endpoint,
|
||||
`Welcome to github-playground!`
|
||||
|
||||
With the new frontend system, plugins are auto-discovered when installed as
|
||||
dependencies of your `packages/app` package. The plugin was already added there
|
||||
by `yarn new`, so the route and a sidebar item are available without any manual
|
||||
wiring in `App.tsx` or `Root.tsx`.
|
||||
|
||||
## The Identity
|
||||
|
||||
Our first modification will be to extract information from the Identity API.
|
||||
|
||||
1. Start by opening
|
||||
`root: plugins > github-playground > src > components > ExampleComponent > ExampleComponent.tsx`
|
||||
1. Add two new imports
|
||||
|
||||
```tsx
|
||||
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
```
|
||||
|
||||
3. Adjust the ExampleComponent from inline to block
|
||||
|
||||
_from inline:_
|
||||
|
||||
```tsx
|
||||
const ExampleComponent = () => ( ... )
|
||||
```
|
||||
|
||||
_to block:_
|
||||
|
||||
```tsx
|
||||
const ExampleComponent = () => {
|
||||
|
||||
return (
|
||||
...
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
4. Now add our hook and const data before the return statement
|
||||
|
||||
```tsx
|
||||
const identityApi = useApi(identityApiRef);
|
||||
|
||||
const userId = identityApi.getUserId();
|
||||
const profile = identityApi.getProfile();
|
||||
```
|
||||
|
||||
5. Finally, update the InfoCard's jsx to use our new data
|
||||
|
||||
```tsx
|
||||
<InfoCard title={userId}>
|
||||
<Typography variant="body1">
|
||||
{`${profile.displayName} | ${profile.email}`}
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
```
|
||||
|
||||
If everything is saved, you should see your name, id, and email on the
|
||||
github-playground page. Our data accessed is synchronous. So we just grab and
|
||||
go.
|
||||
|
||||
https://github.com/backstage/backstage/tree/master/contrib
|
||||
|
||||
6. Here is the entire file for reference
|
||||
[ExampleComponent.tsx](https://github.com/backstage/backstage/tree/master/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md)
|
||||
|
||||
## The Wipe
|
||||
|
||||
The last file we will touch is ExampleFetchComponent. Because of the number of
|
||||
changes, let's start by wiping this component clean.
|
||||
|
||||
1. Start by opening
|
||||
`root: plugins > github-playground > src > components > ExampleFetchComponent > ExampleFetchComponent.tsx`
|
||||
1. Replace everything in the file with the following:
|
||||
|
||||
```tsx
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { Table, TableColumn, Progress } from '@backstage/core-components';
|
||||
import { githubAuthApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
|
||||
export const ExampleFetchComponent = () => {
|
||||
return <div>Nothing to see yet</div>;
|
||||
};
|
||||
```
|
||||
|
||||
3. Save that and ensure you see no errors. Comment out the unused imports if
|
||||
your linter gets in the way.
|
||||
|
||||
###### We will add a lot to this file for the sake of ease. Please don't do this in productional code!
|
||||
|
||||
## The Graph Model
|
||||
|
||||
GitHub has a GraphQL API available for interacting. Let's start by adding our
|
||||
basic repository query
|
||||
|
||||
1. Add the query const statement outside ExampleFetchComponent
|
||||
|
||||
```tsx
|
||||
const query = `{
|
||||
viewer {
|
||||
repositories(first: 100) {
|
||||
totalCount
|
||||
nodes {
|
||||
name
|
||||
createdAt
|
||||
description
|
||||
diskUsage
|
||||
isFork
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
```
|
||||
|
||||
2. Using this structure as a guide, we will break our query into type parts
|
||||
3. Add the following outside of ExampleFetchComponent
|
||||
|
||||
```tsx
|
||||
type Node = {
|
||||
name: string;
|
||||
createdAt: string;
|
||||
description: string;
|
||||
diskUsage: number;
|
||||
isFork: boolean;
|
||||
};
|
||||
|
||||
type Viewer = {
|
||||
repositories: {
|
||||
totalCount: number;
|
||||
nodes: Node[];
|
||||
pageInfo: {
|
||||
endCursor: string;
|
||||
hasNextPage: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## The Table Model
|
||||
|
||||
Using Backstage's own component library, let's define a custom table. This
|
||||
component will get used if we have data to display.
|
||||
|
||||
1. Add the following outside of ExampleFetchComponent
|
||||
|
||||
```tsx
|
||||
type DenseTableProps = {
|
||||
viewer: Viewer;
|
||||
};
|
||||
|
||||
export const DenseTable = ({ viewer }: DenseTableProps) => {
|
||||
const columns: TableColumn[] = [
|
||||
{ title: 'Name', field: 'name' },
|
||||
{ title: 'Created', field: 'createdAt' },
|
||||
{ title: 'Description', field: 'description' },
|
||||
{ title: 'Disk Usage', field: 'diskUsage' },
|
||||
{ title: 'Fork', field: 'isFork' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={viewer.repositories.nodes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## The Fetch
|
||||
|
||||
We're ready to flush out our fetch component
|
||||
|
||||
1. Add our api hook inside ExampleFetchComponent
|
||||
|
||||
```tsx
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
```
|
||||
|
||||
2. The access token we need to make our GitHub request and the request itself is
|
||||
obtained in an asynchronous manner.
|
||||
3. Add the `useAsync` block inside the ExampleFetchComponent
|
||||
|
||||
```tsx
|
||||
const { value, loading, error } = useAsync(async (): Promise<any> => {
|
||||
const token = await auth.getAccessToken();
|
||||
|
||||
const gqlEndpoint = graphql.defaults({
|
||||
// Uncomment baseUrl if using enterprise
|
||||
// baseUrl: 'https://github.MY-BIZ.com/api',
|
||||
headers: {
|
||||
authorization: `token ${token}`,
|
||||
},
|
||||
});
|
||||
const { viewer } = await gqlEndpoint(query);
|
||||
return viewer;
|
||||
}, []);
|
||||
```
|
||||
|
||||
4. The resolved data is conveniently destructured with `value` containing our
|
||||
Viewer type. `loading` as a boolean, self explanatory. And `error` which is
|
||||
present only if necessary. So let's use those as the first 3 of 4 multi
|
||||
return statements.
|
||||
5. Add the _if return_ blocks below our async block
|
||||
|
||||
```tsx
|
||||
if (loading) return <Progress />;
|
||||
if (error) return <Alert severity="error">{error.message}</Alert>;
|
||||
if (value && value.repositories) return <DenseTable viewer={value} />;
|
||||
```
|
||||
|
||||
6. The third line here utilizes our custom table accepting our Viewer type.
|
||||
7. Finally, we add our _else return_ block to catch any other scenarios.
|
||||
|
||||
```tsx
|
||||
return (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={[]}
|
||||
data={[]}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
8. After saving that, and given we don't have any errors, you should see a table
|
||||
with basic information on your repositories.
|
||||
9. Here is the entire file for reference
|
||||
[ExampleFetchComponent.tsx](https://github.com/backstage/backstage/tree/master/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md)
|
||||
10. We finished! You should see your own GitHub repository's information
|
||||
displayed in a basic table. If you run into issues, you can compare the repo
|
||||
that backs this document,
|
||||
[simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin)
|
||||
|
||||
## Where to go from here
|
||||
|
||||
> Break apart ExampleFetchComponent into smaller logical parts contained in
|
||||
> their own files. Rename your components to something other than ExampleXxx.
|
||||
>
|
||||
> You might be really proud of a plugin you develop. Consider sharing it with
|
||||
> the Backstage community by contributing to the [community-plugins repository](https://github.com/backstage/community-plugins).
|
||||
@@ -655,7 +655,6 @@ export default {
|
||||
description: 'Technical tutorials and guides.',
|
||||
},
|
||||
[
|
||||
'tutorials/quickstart-app-plugin',
|
||||
'tutorials/configuring-plugin-databases',
|
||||
'tutorials/manual-knex-rollback',
|
||||
'tutorials/switching-sqlite-postgres',
|
||||
|
||||
@@ -221,7 +221,6 @@ nav:
|
||||
- React Router 6.0 Migration: 'tutorials/react-router-stable-migration.md'
|
||||
- Package Role Migration: 'tutorials/package-role-migration.md'
|
||||
- Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md'
|
||||
- Adding Custom Plugin to Existing Monorepo App: 'tutorials/quickstart-app-plugin.md'
|
||||
- Manual Rollback using Knex: 'tutorials/manual-knex-rollback.md'
|
||||
- Switching Backstage from SQLite to PostgreSQL: 'tutorials/switching-sqlite-postgres.md'
|
||||
- Using the Backstage Proxy from Within a Plugin: 'tutorials/using-backstage-proxy-within-plugin.md'
|
||||
|
||||
@@ -5651,7 +5651,7 @@
|
||||
- `step`: The name of the step that was run
|
||||
- `result`: A string describing whether the task ran successfully, failed, or was skipped
|
||||
|
||||
You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/prometheus-metrics.md
|
||||
You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/384b7bac2e/contrib/docs/tutorials/prometheus-metrics.md
|
||||
|
||||
- 5921b5ce49: - The GitLab Project ID for the `publish:gitlab:merge-request` action is now passed through the query parameter `project` in the `repoUrl`. It still allows people to not use the `projectid` and use the `repoUrl` with the `owner` and `repo` query parameters instead. This makes it easier to publish to repositories instead of writing the full path to the project.
|
||||
- 5025d2e8b6: Adds the ability to pass (an optional) array of strings that will be applied to the newly scaffolded repository as topic labels.
|
||||
@@ -5744,7 +5744,7 @@
|
||||
- `step`: The name of the step that was run
|
||||
- `result`: A string describing whether the task ran successfully, failed, or was skipped
|
||||
|
||||
You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/prometheus-metrics.md
|
||||
You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/384b7bac2e/contrib/docs/tutorials/prometheus-metrics.md
|
||||
|
||||
### Patch Changes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user