diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md deleted file mode 100644 index 8c858bea0f..0000000000 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ /dev/null @@ -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 ( - { - 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; -} - -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 }), - }), - ], -}); -``` diff --git a/contrib/docs/tutorials/prometheus-metrics-output.png b/contrib/docs/tutorials/prometheus-metrics-output.png deleted file mode 100644 index f7fac739ac..0000000000 Binary files a/contrib/docs/tutorials/prometheus-metrics-output.png and /dev/null differ diff --git a/contrib/docs/tutorials/prometheus-metrics.md b/contrib/docs/tutorials/prometheus-metrics.md deleted file mode 100644 index 0db97c649e..0000000000 --- a/contrib/docs/tutorials/prometheus-metrics.md +++ /dev/null @@ -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: - - ![Prometheus Metrics Example Output](prometheus-metrics-output.png) - -## 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 diff --git a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md deleted file mode 100644 index 78b76cd1a2..0000000000 --- a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md +++ /dev/null @@ -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 ( - -
- - -
- - - A description of your plugin goes here. - - - - - - {`${profile.displayName} | ${profile.email}`} - - - - - - - - -
- ); -}; -``` diff --git a/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md b/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md deleted file mode 100644 index d2c0553e34..0000000000 --- a/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md +++ /dev/null @@ -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 ( - - ); -}; - -export const ExampleFetchComponent = () => { - const auth = useApi(githubAuthApiRef); - - const { value, loading, error } = useAsync(async (): Promise => { - 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 ; - if (error) return {error.message}; - if (value && value.repositories) return ; - - return ( -
- ); -}; -``` diff --git a/contrib/kubernetes/basic_kubernetes_example_with_helm/README.md b/contrib/kubernetes/basic_kubernetes_example_with_helm/README.md index c18329f59e..6928fbb568 100644 --- a/contrib/kubernetes/basic_kubernetes_example_with_helm/README.md +++ b/contrib/kubernetes/basic_kubernetes_example_with_helm/README.md @@ -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. diff --git a/contrib/search/confluence/README.md b/contrib/search/confluence/README.md index 80beb9c169..a468aa8e86 100644 --- a/contrib/search/confluence/README.md +++ b/contrib/search/confluence/README.md @@ -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) for this. + 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: diff --git a/docs/service_specification.schema.json b/docs/service_specification.schema.json deleted file mode 100644 index a17f67d30a..0000000000 --- a/docs/service_specification.schema.json +++ /dev/null @@ -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"] - } - } - } - } -} diff --git a/docs/tutorials/quickstart-app-plugin.md b/docs/tutorials/quickstart-app-plugin.md deleted file mode 100644 index 20e2cea453..0000000000 --- a/docs/tutorials/quickstart-app-plugin.md +++ /dev/null @@ -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 - - - {`${profile.displayName} | ${profile.email}`} - - -``` - -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
Nothing to see yet
; -}; -``` - -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 ( -
- ); -}; -``` - -## 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 => { - 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 ; -if (error) return {error.message}; -if (value && value.repositories) return ; -``` - -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 ( -
-); -``` - -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).