Merge remote-tracking branch 'origin/master' into add-additional-scaffolder-permissions
* origin/master: (62 commits) microsite/data: update plugin author Update docs/features/software-catalog/extending-the-model.md Updated the note format of documents beps: move docs to 0008 Apply suggestions from code review microsite: switch feedback rating mode to stars Update ibm-apic-backend.yaml Update .changeset/cold-seas-end.md Update config to reflect valid option Updated the features documents Sort entries in table Incorporated the feedback move over most trivial services to backend-defaults luxon types remove the unneeded copy of the task scheduler interface remove backend-tasks dependency changesets arrange in backend-plugin-api and backend-defaults deprecate everything in backend-tasks add the scheduler definitions to backend-plugin-api/scheduler ... Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-tasks': patch
|
||||
---
|
||||
|
||||
Marked all exports as deprecated and pointed at `@backstage/backend-plugin-api` and `@backstage/backend-defaults`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Updated configuration schema to include the `useRedisSets` cache config option.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-github': patch
|
||||
---
|
||||
|
||||
Added handling for dry run to githubPullRequest and githubWebhook and added tests for this functionality
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
'@backstage/plugin-proxy-backend': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The proxy backend plugin is now protected by Backstage auth, by
|
||||
default. Unless specifically configured (see below), all proxy endpoints will
|
||||
reject requests immediately unless a valid Backstage user or service token is
|
||||
passed along with the request. This aligns the proxy with how other Backstage
|
||||
backends behave out of the box, and serves to protect your upstreams from
|
||||
unauthorized access.
|
||||
|
||||
A proxy configuration section can now look as follows:
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
endpoints:
|
||||
'/pagerduty':
|
||||
target: https://api.pagerduty.com
|
||||
credentials: require # NEW!
|
||||
headers:
|
||||
Authorization: Token token=${PAGERDUTY_TOKEN}
|
||||
```
|
||||
|
||||
There are three possible `credentials` settings at this point:
|
||||
|
||||
- `require`: Callers must provide Backstage user or service credentials with
|
||||
each request. The credentials are not forwarded to the proxy target.
|
||||
- `forward`: Callers must provide Backstage user or service credentials with
|
||||
each request, and those credentials are forwarded to the proxy target.
|
||||
- `dangerously-allow-unauthenticated`: No Backstage credentials are required to
|
||||
access this proxy target. The target can still apply its own credentials
|
||||
checks, but the proxy will not help block non-Backstage-blessed callers. If
|
||||
you also add `allowedHeaders: ['Authorization']` to an endpoint configuration,
|
||||
then the Backstage token (if provided) WILL be forwarded.
|
||||
|
||||
The value `dangerously-allow-unauthenticated` was the old default.
|
||||
|
||||
The value `require` is the new default, so requests that were previously
|
||||
permitted may now start resulting in `401 Unauthorized` responses. If you have
|
||||
`backend.auth.dangerouslyDisableDefaultAuthPolicy` set to `true`, this does not
|
||||
apply; the proxy will behave as if all endpoints were set to
|
||||
`dangerously-allow-unauthenticated`.
|
||||
|
||||
If you have proxy endpoints that require unauthenticated access still, please
|
||||
add `credentials: dangerously-allow-unauthenticated` to their declarations in
|
||||
your app-config.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
`TechDocsIndexPage` now accepts an optional `ownerPickerMode` for toggling the behavior of the `EntityOwnerPicker`, exposing a new mode `<TechDocsIndexPage ownerPickerMode="all" />` particularly suitable for larger catalogs. In this new mode, `EntityOwnerPicker` will display all the users and groups present in the catalog.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Added core service factories and implementations from
|
||||
`@backstage/backend-app-api`. They are now available as subpath exports, e.g.
|
||||
`@backstage/backend-defaults/scheduler` is where the service factory and default
|
||||
implementation of `coreServices.scheduler` now lives. They have been marked as
|
||||
deprecated in their old locations.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
---
|
||||
|
||||
Moved the declaration of the `SchedulerService` here, along with prefixed versions of all of the types it depends on, from `@backstage/backend-tasks`
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Deprecated core service factories and implementations and moved them over to
|
||||
subpath exports on `@backstage/backend-defaults` instead. E.g.
|
||||
`@backstage/backend-defaults/scheduler` is where the service factory and default
|
||||
implementation of `coreServices.scheduler` now lives.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Updating the logger redaction message to something less dramatic
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': patch
|
||||
---
|
||||
|
||||
`DefaultApiExplorerPage` now accepts an optional `ownerPickerMode` for toggling the behavior of the `EntityOwnerPicker`, exposing a new mode `<DefaultApiExplorerPage ownerPickerMode="all" />` particularly suitable for larger catalogs. In this new mode, `EntityOwnerPicker` will display all the users and groups present in the catalog.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Change owner to project for azure host
|
||||
@@ -447,6 +447,7 @@ unregistering
|
||||
unregistration
|
||||
untracked
|
||||
upsert
|
||||
upstreams
|
||||
upvote
|
||||
URIs
|
||||
url
|
||||
|
||||
@@ -22,14 +22,15 @@ Team: @backstage/catalog-maintainers
|
||||
|
||||
Scope: The catalog plugin and catalog model
|
||||
|
||||
| Name | Organization | Team | GitHub | Discord |
|
||||
| --------------- | ------------ | --------- | ----------------------------------------- | ------------------- |
|
||||
| Rickard Dybeck | Spotify | Chipmunks | [alde](https://github.com/alde) | `rdybeck#8083` |
|
||||
| Mike Blockley | Spotify | Chipmunks | [mikeyhc](https://github.com/mikeyhc) | `mikey-spot#5363` |
|
||||
| Elon Jefferson | Spotify | Chipmunks | [Edje-C](https://github.com/Edje-C) | `elon-spotty#6086 ` |
|
||||
| Nurit Izrailov | Spotify | Chipmunks | [nuritizra](https://github.com/nuritizra) | - |
|
||||
| Hunter Dougless | Spotify | Chipmunks | [hntrdglss](https://github.com/hntrdglss) | `hntrdglss#1849` |
|
||||
| Seve Kim | Spotify | Chipmunks | [sevedkim](https://github.com/sevedkim) | `seve#9951` |
|
||||
| Name | Organization | Team | GitHub | Discord |
|
||||
| -------------------- | ------------ | ------------- | ----------------------------------------------- | --------------- |
|
||||
| Ben Lambert | Spotify | Cubic Belugas | [benjdlambert](https://github.com/benjdlambert) | `blam#2159` |
|
||||
| Camila Loiola | Spotify | Cubic Belugas | [camilaibs](http://github.com/camilaibs) | `camilal#0226` |
|
||||
| Fredrik Adelöw | Spotify | Cubic Belugas | [freben](https://github.com/freben) | `freben#3926` |
|
||||
| Johan Haals | Spotify | Cubic Belugas | [jhaals](https://github.com/jhaals) | `Johan#0679` |
|
||||
| Mihai Tabara | Spotify | Cubic Belugas | [MihaiTabara](http://github.com/MihaiTabara) | `mihait#3107` |
|
||||
| Patrik Oldsberg | Spotify | Cubic Belugas | [Rugvip](https://github.com/Rugvip) | `Rugvip#0019` |
|
||||
| Vincenzo Scamporlino | Spotify | Cubic Belugas | [vinzscam](http://github.com/vinzscam) | `vinzscam#6944` |
|
||||
|
||||
### Discoverability
|
||||
|
||||
@@ -73,14 +74,15 @@ Team: @backstage/permission-maintainers
|
||||
|
||||
Scope: The Permission Framework and plugins integrating with the permission framework
|
||||
|
||||
| Name | Organization | Team | GitHub | Discord |
|
||||
| -------------------- | ------------ | --------------- | ------------------------------------------ | ------------- |
|
||||
| Ainhoa Larumbe | Spotify | Imaginary Goats | [ainhoaL](http://github.com/ainhoaL) | ainhoa#8085 |
|
||||
| Eric Peterson | Spotify | Imaginary Goats | [iamEAP](http://github.com/iamEAP) | iamEAP#3058 |
|
||||
| Harry Hogg | Spotify | Imaginary Goats | [HHogg](http://github.com/HHogg) | simplex#3451 |
|
||||
| Joon Park | Spotify | Imaginary Goats | [Joonpark13](http://github.com/Joonpark13) | Sixpool#5060 |
|
||||
| Mike Lewis | Spotify | Imaginary Goats | [mtlewis](http://github.com/mtlewis) | mtlewis#3658 |
|
||||
| Vincenzo Scamporlino | Spotify | Imaginary Goats | [vinzscam](http://github.com/vinzscam) | vinzscam#6944 |
|
||||
| Name | Organization | Team | GitHub | Discord |
|
||||
| -------------------- | ------------ | ------------- | ----------------------------------------------- | --------------- |
|
||||
| Ben Lambert | Spotify | Cubic Belugas | [benjdlambert](https://github.com/benjdlambert) | `blam#2159` |
|
||||
| Camila Loiola | Spotify | Cubic Belugas | [camilaibs](http://github.com/camilaibs) | `camilal#0226` |
|
||||
| Fredrik Adelöw | Spotify | Cubic Belugas | [freben](https://github.com/freben) | `freben#3926` |
|
||||
| Johan Haals | Spotify | Cubic Belugas | [jhaals](https://github.com/jhaals) | `Johan#0679` |
|
||||
| Mihai Tabara | Spotify | Cubic Belugas | [MihaiTabara](http://github.com/MihaiTabara) | `mihait#3107` |
|
||||
| Patrik Oldsberg | Spotify | Cubic Belugas | [Rugvip](https://github.com/Rugvip) | `Rugvip#0019` |
|
||||
| Vincenzo Scamporlino | Spotify | Cubic Belugas | [vinzscam](http://github.com/vinzscam) | `vinzscam#6944` |
|
||||
|
||||
### TechDocs
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ If the notification status is updated, the signal service shall emit a signal wi
|
||||
|
||||
The role of the notifications plugin is to manage the lifecycle of notifications. The backend plugin provides an API for other backends to send notifications, as well as an accompanying [backend service](https://backstage.io/docs/backend-system/architecture/services). It also provides a separate API for the frontend plugin to read notifications for an individual user and manage the read status of notifications.
|
||||
|
||||
The notification backend stores notification using the [database service](https://backstage.io/docs/backend-system/core-services/index#database). In particular it needs to store the following information for each notification:
|
||||
The notification backend stores notification using the [database service](https://backstage.io/docs/backend-system/core-services/index#database). In particular, it needs to store the following information for each notification:
|
||||
|
||||
- ID
|
||||
- Recipients
|
||||
@@ -110,8 +110,9 @@ The notification backend stores notification using the [database service](https:
|
||||
- Topic (optional)
|
||||
- Scope (optional)
|
||||
- Icon (optional)
|
||||
- Metadata (optional)
|
||||
|
||||
The recipients is **not** a list of users, but rather a filter that describes who should receive the notification. It must be possible to evaluate this filter in a database query, so that we can efficiently fetch all notifications for a given user. The same filter will also be used by the signal backend to determine which users should receive a signal.
|
||||
The recipients are **not** a list of users, but rather a filter that describes who should receive the notification. It must be possible to evaluate this filter in a database query, so that we can efficiently fetch all notifications for a given user. The same filter will also be used by the signal backend to determine which users should receive a signal.
|
||||
|
||||
The read date is a timestamp of marking the notifications as read by the user. If missing, the notification is still unread.
|
||||
|
||||
@@ -147,6 +148,8 @@ The link is a relative or absolute URL. As an example, it can be used:
|
||||
- by an external system to request an action within an asynchronous task
|
||||
- by a BE plugin to provide link to other part of the Backstage UI (i.e. to the Catalog)
|
||||
|
||||
The metadata is an opaque JSON field, where an additional payload can be stored. The format of this data is owned by the notification sender and is tied to the notification topic, i.e. notifications sent from the source on the same topic should use a compatible metadata format. The primary purpose of this field is to allow for custom processing and rendering based on the additional metadata.
|
||||
|
||||
The additional links are an array of title-URL pairs. They can represent immediate actions on the notification (i.e. yes-no) or lead the user to additional details.
|
||||
|
||||
The `notification-backend` does not provide any new permissions, since creating notifications can only be done by other backend plugins, while reading notifications can only be done by the authenticated user. It is possible that we want to add a permissions for reading notifications, in particular for admin and impersonation use cases, but that is not part of this proposal or the initial implementation.
|
||||
@@ -206,12 +209,16 @@ export type NotificationSeverity = 'critical' | 'high' | 'normal' | 'low';
|
||||
export type NotificationPayload = {
|
||||
title: string;
|
||||
description?: string;
|
||||
link: string;
|
||||
link?: string;
|
||||
additionalLinks?: string[];
|
||||
severity: NotificationSeverity;
|
||||
severity?: NotificationSeverity;
|
||||
topic?: string;
|
||||
scope?: string;
|
||||
icon?: string;
|
||||
metadata?: Array<{
|
||||
type: string;
|
||||
value: JsonValue;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type Notification = {
|
||||
@@ -236,7 +243,8 @@ interface NotificationService {
|
||||
}
|
||||
```
|
||||
|
||||
Each notification contains a human readable `title`, `origin` and optionally `link` for additional details. The `created`, `id`, `read` and `saved` properties are handled by the backend based and cannot be passed during the notification creation.
|
||||
Each notification contains a human-readable `title`, `origin` and optionally `link` for additional details. The `created`, `id`, `read` and `saved` properties are handled by the backend based and cannot be passed during the notification creation.
|
||||
Any optional additional details could be stored in `metadata`. We advise to provide the name to the type which contains the information about the context and the version, for example: 'core.icon.v1'.
|
||||
|
||||
Calling `sendNotification` should never throw an error so that it doesn't block the current processing. Notifications should be considered as second-level citizens that are not critical if not delivered.
|
||||
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
---
|
||||
title: Improved Backstage Documentation with Personas
|
||||
status: provisional
|
||||
authors:
|
||||
- '@waldirmontoya25'
|
||||
- '@aramissennyeyd'
|
||||
owners:
|
||||
project-areas:
|
||||
- core
|
||||
creation-date: 2024-03-18
|
||||
---
|
||||
|
||||
# BEP: Enhancing Backstage Documentation: Personas, Framework, and Developer Portal
|
||||
|
||||
[**Discussion Issue**](https://github.com/backstage/backstage/issues/23689)
|
||||
|
||||
- [BEP: Enhancing Backstage Documentation: Personas, Framework, and Developer Portal](#bep-enhancing-backstage-documentation-personas-framework-and-developer-portal)
|
||||
- [Summary](#summary)
|
||||
- [Motivation](#motivation)
|
||||
- [Goals](#goals)
|
||||
- [Non-Goals](#non-goals)
|
||||
- [Proposal](#proposal)
|
||||
- [Design Details](#design-details)
|
||||
- [Personas](#personas)
|
||||
- [User](#user)
|
||||
- [Documentation Style](#documentation-style)
|
||||
- [Administrator](#administrator)
|
||||
- [Documentation Style](#documentation-style-1)
|
||||
- [Integrator](#integrator)
|
||||
- [Documentation Style](#documentation-style-2)
|
||||
- [Contributor](#contributor)
|
||||
- [Documentation Style](#documentation-style-3)
|
||||
- [Business Stakeholder](#business-stakeholder)
|
||||
- [Documentation Style](#documentation-style-4)
|
||||
- [Release Plan](#release-plan)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Example Table of Contents](#example-table-of-contents)
|
||||
|
||||
## Summary
|
||||
|
||||
This BEP proposes restructuring the Backstage documentation to emphasize the dual nature of Backstage as both a framework for building developer portals and a fully functional developer portal out of the box, as demonstrated by the demo site. The documentation will be divided into two main sections: One focusing on the developer portal that users get with the core plugins, and another on the framework that allows integrators and builders to create their own developer portal. The goal is to improve clarity, navigation, and adoption of Backstage by positioning it as both a ready-to-use developer portal and a framework for building custom developer portals.
|
||||
|
||||
## Motivation
|
||||
|
||||
The current Backstage documentation has been reported to be difficult to navigate, making it challenging for different personas across the DevEx ecosystem to extract value. The CNCF [**assessment**](https://github.com/cncf/techdocs/tree/main/assessments/0008-backstage/) and the resulting [**issues**](https://github.com/backstage/backstage/issues/21893) highlight the need for improvement in the documentation structure.
|
||||
|
||||
### Goals
|
||||
|
||||
- Divide the documentation into two section: Framework and Developer Portal
|
||||
- Define the personas Backstage is targeting
|
||||
- Structure the documentation to cater the different personas
|
||||
- Move the existing content to the appropriate section
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- Rewrite the entire documentation from scratch
|
||||
- Write additional content beyond the scope of the existing documentation
|
||||
|
||||
## Proposal
|
||||
|
||||
The proposed restructuring of the Backstage documentation revolves around two core ideas:
|
||||
|
||||
1. Positioning Backstage as both a framework to build developer portals and a developer portal itself, and splitting the documentation into two main sections:
|
||||
|
||||
- Developer Portal: Focusing on the features, configuration, and usage of the developer portal that users get out of the box with the core plugins.
|
||||
- Framework: Covering the aspects of Backstage as a framework, including guides for integrators and builders who want to create their own developer portal using Backstage.
|
||||
|
||||
2. Defining the personas participating in Backstage adoption journeys to improve documentation navigation. The identified personas are:
|
||||
|
||||
- **End User**: A person who uses Backstage to find information, use plugins, and consume the developer portal.
|
||||
- **Administrator/Operator**: A person who configures, secures, and deploys the developer portal, manages plugins, and oversees the general administration of the developer portal.
|
||||
- **Integrator/Builder**: A person who builds plugins, customizes the code and design, and creates custom-built developer portals based on the Backstage framework. This includes developers and designers and anyone adding new functionality to their own Backstage instance.
|
||||
- **Product Manager/Business stakeholders**: A person who defines the strategy for adopting Backstage, identifies use cases, communicates the value proposition for adopting Backstage and connects the developer portal to the business strategy.
|
||||
- **Contributor**: A person who contributes to the Backstage upstream ecosystem.
|
||||
|
||||
The adoption strategy would be as follows:
|
||||
|
||||
- Create a dedicated page describing the personas Backstage is targeting and the documentation sections that cater to each persona.
|
||||
- Restructure the documentation into the two new sections (Framework and Developer Portal) and redistribute the existing content accordingly.
|
||||
- Publish a blog post announcing the changes, highlighting Backstage's positioning as both a framework and a developer portal, and explaining the benefits of the restructured documentation.
|
||||
|
||||
The benefits of restructuring the documentation according to these ideas include:
|
||||
|
||||
1. Easier navigation and discoverability of information for different personas and use cases.
|
||||
2. Clear separation of runtime and development documentation.
|
||||
3. Simplified process for contributors to determine the appropriate location for new documentation.
|
||||
4. Streamlined Backstage adoption process for new adopters.
|
||||
|
||||
## Design Details
|
||||
|
||||
- The Docs section of the microsite will be divided into two top-level sections: Framework and Developer Portal.
|
||||
- The structure of the Table of Contents will align with the outline proposed in https://github.com/backstage/backstage/issues/21946.
|
||||
|
||||
### Personas
|
||||
|
||||
#### User
|
||||
|
||||
Users navigate the developer portal to access tools, information, and plugins essential for their daily tasks. They rely on Backstage to effortlessly find resources, utilize integrations, and connect with other tools and services within their ecosystem. Their interaction is predominantly with the frontend of the portal, where ease of use, accessibility, and relevant content discovery are critical.
|
||||
|
||||
##### Documentation Style
|
||||
|
||||
Documentation for this persona should be about usability of the portal once it is running. For example:
|
||||
|
||||
- Understanding the mechanics of the Software Catalog
|
||||
- Registering components
|
||||
- Deleting components
|
||||
- How the source of truth is the external tool linked through the plugins
|
||||
- Understand dependencies relations and the overall schema of the catalog
|
||||
- Using available scaffolder actions
|
||||
- Customizing new workflows with available actions
|
||||
- Searching
|
||||
- Using available plugins
|
||||
- Step by step tutorials
|
||||
|
||||
#### Administrator
|
||||
|
||||
Administrators are responsible for the behind-the-scenes technical setup and maintenance of the Backstage portal. This includes deploying the portal, configuring plugins, managing user access, and ensuring the security and performance of the system. They interact with both the frontend and backend, often using command-line tools, administrative dashboards, and configuration files to perform their tasks.
|
||||
|
||||
##### Documentation Style
|
||||
|
||||
Documentation written for this persona should be DevOps technical, assuming a strong DevOps background. The goal with administrator documentation is to give administrators a strong understanding of how to deploy and manage a Backstage Developer Portal, best practices. For example:
|
||||
|
||||
- Installing and upgrading
|
||||
- Configuring
|
||||
- Authentication
|
||||
- Plugins
|
||||
- Ingesting data (users/groups/components, etc)
|
||||
- Installing plugins
|
||||
- Implement Git Flows for the Developer portal
|
||||
- Creating Pipelines for Docs generation
|
||||
- Troubleshooting
|
||||
|
||||
#### Integrator
|
||||
|
||||
Integrators actively work on extending and customizing Backstage. This includes developing new plugins, customizing the UI/UX, and integrating external services or data sources. Their work is deeply technical, involving coding, and engaging with the Backstage community for support and collaboration. They need a deep understanding of the Backstage architecture and APIs, working closely with both the framework's backend and frontend aspects.
|
||||
|
||||
##### Documentation Style
|
||||
|
||||
Documentation written for this persona should be software technical, assuming a strong software background. While we can assume an overall technical knowledge, where possible we should link out to useful guides for the technologies we use, ex: Node.js, express.js, React, etc. The goal with documentation written for integrators is to give them a strong understanding of how to use the Backstage framework to build/evolve a company's Backstage Developer Portal, orient them to get support from the open source community, and prepare them for continuing to deliver value for their Backstage Developer Portal. For example:
|
||||
|
||||
- API references
|
||||
- Frontend and Backend systems
|
||||
- Package architecture
|
||||
- Extending the Software Catalog
|
||||
- Creating custom themes
|
||||
- Integrating new react components
|
||||
- Building custom authentication providers/strategies
|
||||
- Accessibility
|
||||
|
||||
#### Contributor
|
||||
|
||||
Contributors are involved in the development of the Backstage framework itself. They contribute to the core codebase, develop new features, fix bugs, create documentation and maintain the overall health of the project. They are deeply involved in the open-source community, collaborating with maintainers and other contributors to improve the framework and its ecosystem.
|
||||
|
||||
##### Documentation Style
|
||||
|
||||
The goal with documentation written for contributors is to give them a strong understanding of how to contribute to the Backstage framework, orient them to get support from the open source community, and prepare them for continuing to deliver value for the Backstage framework. For example:
|
||||
|
||||
- Contributing to the Backstage framework
|
||||
- Setting up a development environment
|
||||
- Writing tests
|
||||
- Writing documentation
|
||||
|
||||
#### Business Stakeholder
|
||||
|
||||
Business stakeholders use Backstage to align technical capabilities with business goals, monitoring how features and plugins support operational efficiency, developer satisfaction, and strategic objectives. They are involved in defining the strategy and measuring the impact of the developer portal on the organization. They need to navigate through dashboards, reports, and analytics within Backstage to gather insights and make informed decisions.
|
||||
|
||||
##### Documentation Style
|
||||
|
||||
Documentation written for this persona should be strategic, assuming a strong background in business development and strategy. The goal for business documentation is to give a strong understanding of what Backstage Developer Portal can do for their company, how to deliver value quickly and continuously and guides for pitching or driving Backstage adoption. For example:
|
||||
|
||||
- Adoption use cases
|
||||
- Adoption strategies
|
||||
- Measuring success
|
||||
- Case studies
|
||||
|
||||
## Release Plan
|
||||
|
||||
- Release the BEP by 03/24/2024.
|
||||
- Discuss the changes with the community and gather feedback by 04/24/2024.
|
||||
- Implement the changes by 04/30/2024.
|
||||
|
||||
## Dependencies
|
||||
|
||||
None
|
||||
|
||||
## Example Table of Contents
|
||||
|
||||
- Overview
|
||||
- "The overview should introduce users to the concept of Backstage, what an IDP is, how to deliver value, why you should care about DevEx, etc."
|
||||
- What is Backstage?
|
||||
- Roadmap
|
||||
- Vision
|
||||
- Release and Versioning Policy
|
||||
- Backstage Threat Model
|
||||
- Logo assets
|
||||
- Support and community
|
||||
- Framework
|
||||
- Architecture Overview
|
||||
- "The arch overview should explain how the framework is structured, where plugins and instances fit in and how to understand the current design of Backstage."
|
||||
- Getting Started
|
||||
- Integrator/Builder Guides
|
||||
- Local Development
|
||||
- "Prepare users for how to develop locally, debug problems, run tests, etc."
|
||||
- CLI
|
||||
- Linking in local packages
|
||||
- Debugging Backstage
|
||||
- Backstage core framework
|
||||
- "Internal documentation."
|
||||
- Systems
|
||||
- Frontend
|
||||
- Old
|
||||
- New
|
||||
- Backend
|
||||
- Old
|
||||
- New
|
||||
- API Reference
|
||||
- "Internal documentation"
|
||||
- Building plugins
|
||||
- "How to build a plugin, how to integrate it with other plugins, how to deploy and monitor it, and how to iterate on plugin development."
|
||||
- Intro to plugins
|
||||
- Existing plugins
|
||||
- Creating a new plugin
|
||||
- Plugin development
|
||||
- Structuring a plugin
|
||||
- Integrating with other systems
|
||||
- Integrating with the Catalog
|
||||
- Integrating Search
|
||||
- Composability system
|
||||
- Internationalization
|
||||
- Plugin analytics
|
||||
- Feature flags
|
||||
- OpenAPI
|
||||
- Backends and APIs
|
||||
- Testing
|
||||
- Publishing
|
||||
- Core Plugins
|
||||
- "How to leverage the existing plugins for your new plugin or customization options."
|
||||
- Home Page
|
||||
- Customizing the home page
|
||||
- Software Catalog
|
||||
- Extending the model
|
||||
- External integrations
|
||||
- Catalog Customization
|
||||
- API
|
||||
- Software Templates
|
||||
- Writing custom actions
|
||||
- Writing tests for actions
|
||||
- Writing custom field extensions
|
||||
- Writing custom step layouts
|
||||
- Authorizing parameters, steps and actions
|
||||
- Migrating to react-jsonschema-form@v5
|
||||
- Migrating to v1beta3 templates
|
||||
- Search
|
||||
- Overview
|
||||
- Getting Started with search
|
||||
- Search concepts
|
||||
- Search architecture
|
||||
- Search Engines
|
||||
- How to Guides
|
||||
- TechDocs
|
||||
- Customizing TechDocs
|
||||
- TechDocs add-ons
|
||||
- Kubernetes
|
||||
- Customizing the kubernetes plugin
|
||||
- Authentication
|
||||
- Proxy
|
||||
- Permissions
|
||||
- Overview
|
||||
- Concepts
|
||||
- Getting Started
|
||||
- Writing a permission policy
|
||||
- Frontend integration
|
||||
- Defining custom permission rules
|
||||
- Using permissions in plugins
|
||||
- Designing for Backstage
|
||||
- ADRs
|
||||
- Accessibility
|
||||
- References
|
||||
- Contributor Guides
|
||||
- "How to get started contributing to OSS."
|
||||
- Contributing to Backstage
|
||||
- Reference
|
||||
- Developer Portal
|
||||
- Architecture Overview
|
||||
- Getting Started
|
||||
- Administrator Guides
|
||||
- Developer Portal
|
||||
- "How do I deploy, monitor, configure and verify my Backstage Developer Portal?"
|
||||
- Installing and Configuring
|
||||
- Database
|
||||
- Authentication
|
||||
- Installing plugins
|
||||
- Customize the design
|
||||
- Securing
|
||||
- Deploying in Production
|
||||
- Integrating with other systems
|
||||
- Managing
|
||||
- Monitoring
|
||||
- Troubleshooting
|
||||
- Upgrading
|
||||
- Keeping backstage up to date
|
||||
- Customizing
|
||||
- Core Plugins
|
||||
- "How do I install and configure Backstage Developer Portal with plugins."
|
||||
- Home Page
|
||||
- Installing and Configuring
|
||||
- Software Catalog
|
||||
- Overview
|
||||
- The life of an Entity
|
||||
- Catalog Configuration
|
||||
- System Model
|
||||
- YAML file format
|
||||
- Entity Reference
|
||||
- Well Known annotations
|
||||
- Well known relations
|
||||
- Well known statuses
|
||||
- Creating the catalog graph
|
||||
- Software Templates
|
||||
- Overview
|
||||
- Configuring
|
||||
- Adding a new template
|
||||
- Writing a template
|
||||
- Built in actions
|
||||
- TechDocs
|
||||
- Overview
|
||||
- Getting Started
|
||||
- Architecture
|
||||
- Installing and configuring
|
||||
- Using Cloud Storage for TechDocs generated files
|
||||
- Configuring CI/CD to generate and publish TechDocs sites
|
||||
- TechDocs CLI
|
||||
- Troubleshooting
|
||||
- Kubernetes
|
||||
- Installing and Configuring
|
||||
- Authentication
|
||||
- Troubleshooting
|
||||
- Search
|
||||
- Product Manager Guides
|
||||
- "How do I present Backstage to leadership, what are the benefits, why should I care, etc."
|
||||
- Strategies for adopting
|
||||
- Use cases
|
||||
- User Guides
|
||||
- "How do I use the default OSS Backstage"
|
||||
- Logging in
|
||||
- Registering a component
|
||||
- Creating a new component
|
||||
- Reference
|
||||
@@ -4,10 +4,14 @@ title: Contributing New Providers
|
||||
description: Documentation on adding new authentication providers
|
||||
---
|
||||
|
||||
> NOTE: The primary audience for this documentation are contributors to the main
|
||||
> Backstage project that want to add support for new authentication providers.
|
||||
> While you can follow it to implement your own custom providers it is much
|
||||
> more advanced than using our built-in providers.
|
||||
:::note Note
|
||||
|
||||
The primary audience for this documentation are contributors to the main
|
||||
Backstage project that want to add support for new authentication providers.
|
||||
While you can follow it to implement your own custom providers it is much
|
||||
more advanced than using our built-in providers.
|
||||
|
||||
:::
|
||||
|
||||
## How Does Authentication Work?
|
||||
|
||||
|
||||
@@ -156,8 +156,12 @@ payload:
|
||||
- `sub`: the exact string "backstage-server"
|
||||
- `exp`: one hour from the time it was generated, in epoch seconds
|
||||
|
||||
> NOTE: The JWT must encode the `alg` header as a protected header, such as with
|
||||
> [setProtectedHeader](https://github.com/panva/jose/blob/main/docs/classes/jwt_sign.SignJWT.md#setprotectedheader).
|
||||
:::note Note
|
||||
|
||||
The JWT must encode the `alg` header as a protected header, such as with
|
||||
[setProtectedHeader](https://github.com/panva/jose/blob/main/docs/classes/jwt_sign.SignJWT.md#setprotectedheader).
|
||||
|
||||
:::
|
||||
|
||||
The caller then passes along the JWT token with requests in the `Authorization`
|
||||
header:
|
||||
|
||||
@@ -236,7 +236,11 @@ backend.add(kubernetesModuleCustomClusterDiscovery);
|
||||
backend.start();
|
||||
```
|
||||
|
||||
> Note: this example assumes the `CustomClustersSupplier` class is the same from the [previous example](#custom-cluster-discovery)
|
||||
:::note Note
|
||||
|
||||
This example assumes the `CustomClustersSupplier` class is the same from the [previous example](#custom-cluster-discovery)
|
||||
|
||||
:::
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -34,10 +34,14 @@ const searchEngine = new LunrSearchEngine({ logger: env.logger });
|
||||
const indexBuilder = new IndexBuilder({ logger: env.logger, searchEngine });
|
||||
```
|
||||
|
||||
> Note: Lunr is appropriate as a zero-config search engine when developing
|
||||
> other parts of Backstage locally, however its use is highly discouraged when
|
||||
> running Backstage in production. When deploying Backstage, use one of the
|
||||
> other search engines instead.
|
||||
:::note Note
|
||||
|
||||
Lunr is appropriate as a zero-config search engine when developing
|
||||
other parts of Backstage locally, however its use is highly discouraged when
|
||||
running Backstage in production. When deploying Backstage, use one of the
|
||||
other search engines instead.
|
||||
|
||||
:::
|
||||
|
||||
## Postgres
|
||||
|
||||
|
||||
@@ -95,7 +95,11 @@ const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
|
||||
<Route path="/catalog" element={<CatalogIndexPage columns={myColumnsFunc} />} />
|
||||
```
|
||||
|
||||
> Note: the above example has been simplified and you will most likely have more code then just this in your `App.tsx` file.
|
||||
:::note Note
|
||||
|
||||
The above example has been simplified and you will most likely have more code then just this in your `App.tsx` file.
|
||||
|
||||
:::
|
||||
|
||||
## Customize Actions
|
||||
|
||||
@@ -162,7 +166,11 @@ const customActions: TableProps<CatalogTableRow>['actions'] = [
|
||||
<Route path="/catalog" element={<CatalogIndexPage actions={customActions} />} />
|
||||
```
|
||||
|
||||
> Note: the above example has been simplified and you will most likely have more code then just this in your `App.tsx` file.
|
||||
:::note Note
|
||||
|
||||
The above example has been simplified and you will most likely have more code then just this in your `App.tsx` file.
|
||||
|
||||
:::
|
||||
|
||||
The above customization will override the existing actions. Currently the only way to keep them and add your own is to also include the existing actions in your array by copying them from the [`defaultActions`](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx#L113-L168).
|
||||
|
||||
@@ -400,7 +408,11 @@ export const CustomCatalogPage = () => {
|
||||
|
||||
The above is a very basic version of a fully custom `CatalogIndexPage`, you'll want to explore the various props to see what you can all do with them. This was built off the building blocks seen in the [`DefaultCatalogPage`](https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx)
|
||||
|
||||
> Note: The catalog index page is designed to have a minimal code footprint to support easy customization, but creating a replica does introduce a possibility of drifting out of date over time. Be sure to check the catalog [CHANGELOG](https://github.com/backstage/backstage/blob/master/plugins/catalog/CHANGELOG.md) periodically.
|
||||
:::note Note
|
||||
|
||||
The catalog index page is designed to have a minimal code footprint to support easy customization, but creating a replica does introduce a possibility of drifting out of date over time. Be sure to check the catalog [CHANGELOG](https://github.com/backstage/backstage/blob/master/plugins/catalog/CHANGELOG.md) periodically.
|
||||
|
||||
:::
|
||||
|
||||
To use this custom `CatalogIndexPage` which we called `CustomCatalogPage`, you'll need to make the following change:
|
||||
|
||||
|
||||
@@ -0,0 +1,604 @@
|
||||
---
|
||||
id: extending-the-model--old
|
||||
title: Extending the model
|
||||
# prettier-ignore
|
||||
description: Documentation on extending the catalog model
|
||||
---
|
||||
|
||||
The Backstage catalog [entity data model](descriptor-format.md) is based on the
|
||||
[Kubernetes objects format](https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/),
|
||||
and borrows a lot of its semantics as well. This page describes those semantics
|
||||
at a higher level and how to extend them to fit your organization.
|
||||
|
||||
Backstage comes with a number of catalog concepts out of the box:
|
||||
|
||||
- There are a number of builtin versioned _kinds_, such as `Component`, `User`
|
||||
etc. These encapsulate the high level concept of an entity, and define the
|
||||
schema for its entity definition data.
|
||||
- An entity has both a _metadata_ object and a _spec_ object at the root.
|
||||
- Each kind may or may not have a _type_. For example, there are several well
|
||||
known types of component, such as `service` and `website`. These clarify the
|
||||
more detailed nature of the entity, and may affect what features are exposed
|
||||
in the interface.
|
||||
- Entities may have a number of _[annotations](well-known-annotations.md)_ on
|
||||
them. These can be added either by humans into the descriptor files, or added
|
||||
by automated processes when the entity is ingested into the catalog.
|
||||
- Entities may have a number of _labels_ on them.
|
||||
- Entities may have a number of _relations_, expressing how they relate to each
|
||||
other in different ways.
|
||||
|
||||
We'll list different possibilities for extending this below.
|
||||
|
||||
## Adding a New apiVersion of an Existing Kind
|
||||
|
||||
Example intents:
|
||||
|
||||
> "I want to evolve this core kind, tweaking the semantics a bit so I will bump
|
||||
> the apiVersion a step"
|
||||
|
||||
> "This core kind is a decent fit but we want to evolve it at will so we'll move
|
||||
> it to our own company's apiVersion space and use that instead of
|
||||
> `backstage.io`."
|
||||
|
||||
The `backstage.io` apiVersion space is reserved for use by the Backstage
|
||||
maintainers. Please do not change or add versions within that space.
|
||||
|
||||
If you add an [apiVersion](descriptor-format.md#apiversion-and-kind-required)
|
||||
space of your own, you are effectively branching out from the underlying kind
|
||||
and making your own. An entity kind is identified by the apiVersion + kind pair,
|
||||
so even though the resulting entity may be similar to the core one, there will
|
||||
be no guarantees that plugins will be able to parse or understand its data. See
|
||||
below about adding a new kind.
|
||||
|
||||
## Adding a New Kind
|
||||
|
||||
Example intents:
|
||||
|
||||
> "The kinds that come with the package are lacking. I want to model this other
|
||||
> thing that is a poor fit for either of the builtins."
|
||||
|
||||
> "This core kind is a decent fit but we want to evolve it at will so we'll move
|
||||
> it to our own company's apiVersion space and use that instead of
|
||||
> `backstage.io`."
|
||||
|
||||
A [kind](descriptor-format.md#apiversion-and-kind-required) is an overarching
|
||||
family, or an idea if you will, of entities that also share a schema. Backstage
|
||||
comes with a number of builtin ones that we believe are useful for a large
|
||||
variety of needs that one may want to model in Backstage. The primary ambition
|
||||
is to map things to these kinds, but sometimes you may want or need to extend
|
||||
beyond them.
|
||||
|
||||
Introducing a new apiVersion is basically the same as adding a new kind. Bear in
|
||||
mind that most plugins will be compiled against the builtin
|
||||
`@backstage/catalog-model` package and have expectations that kinds align with
|
||||
that.
|
||||
|
||||
The catalog backend itself, from a storage and API standpoint, does not care
|
||||
about the kind of entities it stores. Extending with new kinds is mainly a
|
||||
matter of permitting them to pass validation when building the backend catalog
|
||||
using the `CatalogBuilder`, and then to make plugins be able to understand the
|
||||
new kind.
|
||||
|
||||
For the consuming side, it's a different story. Adding a kind has a very large
|
||||
impact. The very foundation of Backstage is to attach behavior and views and
|
||||
functionality to entities that we ascribe some meaning to. There will be many
|
||||
places where code checks `if (kind === 'X')` for some hard coded `X`, and casts
|
||||
it to a concrete type that it imported from a package such as
|
||||
`@backstage/catalog-model`.
|
||||
|
||||
If you want to model something that doesn't feel like a fit for either of the
|
||||
builtin kinds, feel free to reach out to the Backstage maintainers to discuss
|
||||
how to best proceed.
|
||||
|
||||
If you end up adding that new kind, you must namespace its `apiVersion`
|
||||
accordingly with a prefix that makes sense, typically based on your organization
|
||||
name - e.g. `my-company.net/v1`. Also do pick a new `kind` identifier that does
|
||||
not collide with the builtin kinds.
|
||||
|
||||
## Adding a New Type of an Existing Kind
|
||||
|
||||
Example intents:
|
||||
|
||||
> "This is clearly a component, but it's of a type that doesn't quite fit with
|
||||
> the ones I've seen before."
|
||||
|
||||
> "We don't call our teams "team", can't we put "flock" as the group type?"
|
||||
|
||||
Some entity kinds have a `type` field in its spec. This is where an organization
|
||||
are free to express the variety of entities within a kind. This field is
|
||||
expected to follow some taxonomy that makes sense for yourself. The chosen value
|
||||
may affect what operations and views are enabled in Backstage for that entity.
|
||||
Inside Spotify our model has grown significantly over the years, and our
|
||||
component types now include ML models, apps, data pipelines and many more.
|
||||
|
||||
It might be tempting to put software that doesn't fit into any of the existing
|
||||
types into an Other catch-all type. There are a few reasons why we advise
|
||||
against this; firstly, we have found that it is preferred to match the
|
||||
conceptual model that your engineers have when describing your software.
|
||||
Secondly, Backstage helps your engineers manage their software by integrating
|
||||
the infrastructure tooling through plugins. Different plugins are used for
|
||||
managing different types of components.
|
||||
|
||||
For example, the
|
||||
[Lighthouse plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/lighthouse/plugins/lighthouse)
|
||||
only makes sense for Websites. The more specific you can be in how you model
|
||||
your software, the easier it is to provide plugins that are contextual.
|
||||
|
||||
Adding a new type takes relatively little effort and carries little risk. Any
|
||||
type value is accepted by the catalog backend, but plugins may have to be
|
||||
updated if you want particular behaviors attached to that new type.
|
||||
|
||||
## Changing the Validation Rules for The Entity Envelope or Metadata Fields
|
||||
|
||||
Example intents:
|
||||
|
||||
> "We want to import our old catalog but the default set of allowed characters
|
||||
> for a metadata.name are too strict."
|
||||
|
||||
> "I want to change the rules for annotations so that I'm allowed to store any
|
||||
> data in annotation values, not just strings."
|
||||
|
||||
After pieces of raw entity data have been read from a location, they are passed
|
||||
through a field format validation step. This ensures that the types and syntax
|
||||
of the base envelope and metadata make sense - in short, things that aren't
|
||||
entity-kind-specific. Some or all of these validators can be replaced when
|
||||
building the backend using the catalog's dedicated `catalogModelExtensionPoint`
|
||||
(or directly on the `CatalogBuilder` if you are still using the old backend
|
||||
system).
|
||||
|
||||
The risk and impact of this type of extension varies, based on what it is that
|
||||
you want to do. For example, extending the valid character set for kinds,
|
||||
namespaces and names can be fairly harmless, with a few notable exceptions -
|
||||
there is code that expects these to never ever contain a colon or slash, for
|
||||
example, and introducing URL-unsafe characters risks breaking plugins that
|
||||
aren't careful about encoding arguments. Supporting non-strings in annotations
|
||||
may be possible but has not yet been tried out in the real world - there is
|
||||
likely to be some level of plugin breakage that can be hard to predict.
|
||||
|
||||
You must also be careful about not making the rules _more strict_ than they used
|
||||
to be after populating the catalog with data. This risks making previously valid
|
||||
entities start having processing errors and fail to update.
|
||||
|
||||
Before making this kind of extension, we recommend that you contact the
|
||||
Backstage maintainers or a support partner to discuss your use case.
|
||||
|
||||
This is an example of relaxing the format rules of the `metadata.name` field:
|
||||
|
||||
```ts
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
|
||||
|
||||
const myCatalogCustomizations = createBackendModule({
|
||||
pluginId: 'catalog',
|
||||
moduleId: 'catalog-customization',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
catalogModel: catalogModelExtensionPoint,
|
||||
},
|
||||
async init({ catalogModel }) {
|
||||
catalogModel.setFieldValidators({
|
||||
// This is only one of many methods that you can pass into
|
||||
// setFieldValidators; your editor of choice should help you
|
||||
// find the others. The length checks and regexp inside are
|
||||
// just examples and can be adjusted as needed, but take care
|
||||
// to test your changes thoroughly to ensure that you get
|
||||
// them right.
|
||||
isValidEntityName(value) {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.length >= 1 &&
|
||||
value.length <= 63 &&
|
||||
/^[A-Za-z0-9@+_.-]+$/.test(value)
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const backend = createBackend();
|
||||
// ... add other backend features and the catalog backend itself here ...
|
||||
backend.add(myCatalogCustomizations);
|
||||
backend.start();
|
||||
```
|
||||
|
||||
## Changing the Validation Rules for Core Entity Fields
|
||||
|
||||
Example intent:
|
||||
|
||||
> "I don't like that the owner is mandatory. I'd like it to be optional."
|
||||
|
||||
After reading and policy-checked entity data from a location, it is sent through
|
||||
the processor chain looking for processors that implement the
|
||||
`validateEntityKind` step, to see that the data is of a known kind and abides by
|
||||
its schema. There is a builtin processor that implements this for all known core
|
||||
kinds and matches the data against their fixed validation schema. This processor
|
||||
can be replaced when building the backend catalog using the `CatalogBuilder`,
|
||||
with a processor of your own that validates the data differently.
|
||||
This replacement processor must have a name that matches the builtin processor, `BuiltinKindsEntityProcessor`.
|
||||
|
||||
This type of extension is high risk, and may have high impact across the
|
||||
ecosystem depending on the type of change that is made. It is therefore not
|
||||
recommended in normal cases. There will be a large number of plugins and
|
||||
processors - and even the core itself - that make assumptions about the shape of
|
||||
the data and import the typescript data type from the `@backstage/catalog-model`
|
||||
package.
|
||||
|
||||
## Adding New Fields to the Metadata Object
|
||||
|
||||
Example intent:
|
||||
|
||||
> "Our entities have this auxiliary property that I would like to express for
|
||||
> several entity kinds and it doesn't really fit as a spec field."
|
||||
|
||||
The metadata object is currently left open for extension. Any unknown fields
|
||||
found in the metadata will just be stored verbatim in the catalog. However we
|
||||
want to caution against extending the metadata excessively. Firstly, you run the
|
||||
risk of colliding with future extensions to the model. Secondly, it is common
|
||||
that this type of extension lives more comfortably elsewhere - primarily in the
|
||||
metadata labels or annotations, but sometimes you even may want to make a new
|
||||
component type or similar instead.
|
||||
|
||||
There are some situations where metadata can be the right place. If you feel
|
||||
that you have run into such a case and that it would apply to others, do feel
|
||||
free to contact the Backstage maintainers or a support partner to discuss your
|
||||
use case. Maybe we can extend the core model to benefit both you and others.
|
||||
|
||||
## Adding New Fields to the Spec Object of an Existing Kind
|
||||
|
||||
Example intent:
|
||||
|
||||
> "The builtin Component kind is fine but we want to add an additional field to
|
||||
> the spec for describing whether it's in prod or staging."
|
||||
|
||||
A kind's schema validation typically doesn't forbid "unknown" fields in an
|
||||
entity `spec`, and the catalog will happily store whatever is in it. So doing
|
||||
this will usually work from the catalog's point of view.
|
||||
|
||||
Adding fields like this is subject to the same risks as mentioned about metadata
|
||||
extensions above. Firstly, you run the risk of colliding with future extensions
|
||||
to the model. Secondly, it is common that this type of extension lives more
|
||||
comfortably elsewhere - primarily in the metadata labels or annotations, but
|
||||
sometimes you even may want to make a new component type or similar instead.
|
||||
|
||||
There are some situations where the spec can be the right place. If you feel
|
||||
that you have run into such a case and that it would apply to others, do feel
|
||||
free to contact the Backstage maintainers or a support partner to discuss your
|
||||
use case. Maybe we can extend the core model to benefit both you and others.
|
||||
|
||||
## Adding a New Annotation
|
||||
|
||||
Example intents:
|
||||
|
||||
> "Our custom made build system has the concept of a named pipeline-set, and we
|
||||
> want to associate individual components with their corresponding pipeline-sets
|
||||
> so we can show their build status."
|
||||
|
||||
> "We have an alerting system that automatically monitors service health, and
|
||||
> there's this integration key that binds the service to an alerts pool. We want
|
||||
> to be able to show the ongoing alerts for our services in Backstage so it'd be
|
||||
> nice to attach that integration key to the entity somehow."
|
||||
|
||||
Annotations are mainly intended to be consumed by plugins, for feature detection
|
||||
or linking into external systems. Sometimes they are added by humans, but often
|
||||
they are automatically generated at ingestion time by processors. There is a set
|
||||
of [well-known annotations](well-known-annotations.md), but you are free to add
|
||||
additional ones. This carries no risk or impact to other systems as long as you
|
||||
abide by the following naming rules.
|
||||
|
||||
- The `backstage.io` annotation prefix is reserved for use by the Backstage
|
||||
maintainers. Reach out to us if you feel that you would like to make an
|
||||
addition to that prefix.
|
||||
- Annotations that pertain to a well known third party system should ideally be
|
||||
prefixed with a domain, in a way that makes sense to a reader and connects it
|
||||
clearly to the system (or the maker of the system). For example, you might use
|
||||
a `pagerduty.com` prefix for pagerduty related annotations, but maybe not
|
||||
`ldap.com` for LDAP annotations since it's not directly affiliated with or
|
||||
owned by an LDAP foundation/company/similar.
|
||||
- Annotations that have no prefix at all, are considered local to your Backstage
|
||||
instance and can be used freely as such, but you should not make use of them
|
||||
outside of your organization. For example, if you were to open source a plugin
|
||||
that generates or consumes annotations, then those annotations must be
|
||||
properly prefixed with your company domain or a domain that pertains to the
|
||||
annotation at hand.
|
||||
|
||||
## Adding a New Label
|
||||
|
||||
Example intents:
|
||||
|
||||
> "Our process reaping system wants to periodically scrape for components that
|
||||
> have a certain property."
|
||||
|
||||
> "It'd be nice if our service owners could just tag their components somehow to
|
||||
> let the CD system know to automatically generate SRV records or not for that
|
||||
> service."
|
||||
|
||||
Labels are mainly intended to be used for filtering of entities, by external
|
||||
systems that want to find entities that have some certain property. This is
|
||||
sometimes used for feature detection / selection. An example could be to add a
|
||||
label `deployments.my-company.net/register-srv: "true"`.
|
||||
|
||||
At the time of writing this, the use of labels is very limited and we are still
|
||||
settling together with the community on how to best use them. If you feel that
|
||||
your use case fits the labels best, we would appreciate if you let the Backstage
|
||||
maintainers know.
|
||||
|
||||
You are free to add labels. This carries no risk or impact to other systems as
|
||||
long as you abide by the following naming rules.
|
||||
|
||||
- The `backstage.io` label prefix is reserved for use by the Backstage
|
||||
maintainers. Reach out to us if you feel that you would like to make an
|
||||
addition to that prefix.
|
||||
- Labels that pertain to a well known third party system should ideally be
|
||||
prefixed with a domain, in a way that makes sense to a reader and connects it
|
||||
clearly to the system (or the maker of the system). For example, you might use
|
||||
a `pagerduty.com` prefix for pagerduty related labels, but maybe not
|
||||
`ldap.com` for LDAP labels since it's not directly affiliated with or owned by
|
||||
an LDAP foundation/company/similar.
|
||||
- Labels that have no prefix at all, are considered local to your Backstage
|
||||
instance and can be used freely as such, but you should not make use of them
|
||||
outside of your organization. For example, if you were to open source a plugin
|
||||
that generates or consumes labels, then those labels must be properly prefixed
|
||||
with your company domain or a domain that pertains to the label at hand.
|
||||
|
||||
## Adding a New Relation Type
|
||||
|
||||
Example intents:
|
||||
|
||||
> "We have this concept of service maintainership, separate from ownership, that
|
||||
> we would like to make relations to individual users for."
|
||||
|
||||
> "We feel that we want to explicitly model the team-to-global-department
|
||||
> mapping as a relation, because it is core to our org setup and we frequently
|
||||
> query for it."
|
||||
|
||||
Any processor can emit relations for entities as they are being processed, and
|
||||
new processors can be added when building the backend catalog using the
|
||||
`CatalogBuilder`. They can emit relations based on the entity data itself, or
|
||||
based on information gathered from elsewhere. Relations are directed and go from
|
||||
a source entity to a target entity. They are also tied to the entity that
|
||||
originated them - the one that was subject to processing when the relation was
|
||||
emitted. Relations may be dangling (referencing something that does not actually
|
||||
exist by that name in the catalog), and callers need to be aware of that.
|
||||
|
||||
There is a set of [well-known relations](well-known-relations.md), but you are
|
||||
free to emit your own as well. You cannot change the fact that they are directed
|
||||
and have a source and target that have to be an
|
||||
[entity reference](references.md), but you can invent your own types. You do not
|
||||
have to make any changes to the catalog backend in order to accept new relation
|
||||
types.
|
||||
|
||||
At the time of writing this, we do not have any namespacing/prefixing scheme for
|
||||
relation types. The type is also not validated to contain only some particular
|
||||
set of characters. Until rules for this are settled, you should stick to using
|
||||
only letters, dashes and digits, and to avoid collisions with future core
|
||||
relation types, you may want to prefix the type somehow. For example:
|
||||
`myCompany-maintainerOf` + `myCompany-maintainedBy`.
|
||||
|
||||
If you have a suggestion for a relation type to be elevated to the core
|
||||
offering, reach out to the Backstage maintainers or a support partner.
|
||||
|
||||
## Using a Well-Known Relation Type for a New Purpose
|
||||
|
||||
Example intents:
|
||||
|
||||
> "The ownerOf/ownedBy relation types sound like a good fit for expressing how
|
||||
> users are technical owners of our company specific ServiceAccount kind, and we
|
||||
> want to reuse those relation types for that."
|
||||
|
||||
At the time of writing, this is uncharted territory. If the documented use of a
|
||||
relation states that one end of the relation commonly is a User or a Group, for
|
||||
example, then consumers are likely to have conditional statements on the form
|
||||
`if (x.kind === 'User') {} else {}`, which get confused when an unexpected kind
|
||||
appears.
|
||||
|
||||
If you want to extend the use of an established relation type in a way that has
|
||||
an effect outside of your organization, reach out to the Backstage maintainers
|
||||
or a support partner to discuss risk/impact. It may even be that one end of the
|
||||
relation could be considered for addition to the core.
|
||||
|
||||
## Adding a New Status field
|
||||
|
||||
Example intent:
|
||||
|
||||
> "We would like to convey entity statuses through the catalog in a generic way,
|
||||
> as an integration layer. Our monitoring and alerting system has a plugin with
|
||||
> Backstage, and it would be useful if the entity's status field contained the
|
||||
> current alert state close to the actual entity data for anyone to consume. We
|
||||
> find the `status.items` semantics a poor fit, so we would prefer to make our
|
||||
> own custom field under `status` for these purposes."
|
||||
|
||||
We have not yet ventured to define any generic semantics for the `status`
|
||||
object. We recommend sticking with the `status.items` mechanism where possible
|
||||
(see below), since third party consumers will not be able to consume your status
|
||||
information otherwise. Please reach out to the maintainers on Discord or by
|
||||
making a GitHub issue describing your use case if you are interested in this
|
||||
topic.
|
||||
|
||||
## Adding a New Status Item Type
|
||||
|
||||
Example intent:
|
||||
|
||||
> "The semantics of the entity `status.items` field are fine for our needs, but
|
||||
> we want to contribute our own type of status into that array instead of the
|
||||
> catalog specific one."
|
||||
|
||||
This is a simple, low risk way of adding your own status information to
|
||||
entities. Consumers will be able to easily track and display the status together
|
||||
with other types / sources.
|
||||
|
||||
We recommend that any status type that are not strictly private within the
|
||||
organization be namespaced to avoid collisions. Statuses emitted by Backstage
|
||||
core processes will for example be prefixed with `backstage.io/`, your
|
||||
organization may prefix with `my-org.net/`, and `pagerduty.com/active-alerts`
|
||||
could be a sensible complete status item type for that particular external
|
||||
system.
|
||||
|
||||
The mechanics for how to emit custom statuses is not in place yet, so if this is
|
||||
of interest to you, you might consider contacting the maintainers on Discord or
|
||||
my making a GitHub issue describing your use case.
|
||||
[This issue](https://github.com/backstage/backstage/issues/2292) also contains
|
||||
more context.
|
||||
|
||||
## Referencing different environments with the model
|
||||
|
||||
Example intent:
|
||||
|
||||
> "I have multiple versions of my API deployed in different environments so I
|
||||
> want to have `mytool-dev` and `mytool-prod` as different entities."
|
||||
|
||||
While it's possible to have different versions of the same thing represented as
|
||||
separate entities, it's something we generally recommend against. We believe
|
||||
that a developer should be able to just find for example one `Component`
|
||||
representing a service, and to be able to see the different code versions that
|
||||
are deployed throughout your stack within its view. This reasoning works
|
||||
similarly for other kinds as well, such as `API`.
|
||||
|
||||
That being said - sometimes the differences between versions are so large, that
|
||||
they represent what is for all intents and purposes an entirely new entity as
|
||||
seen from the consumer's point of view. This can happen for example for
|
||||
different _significant_ major versions of an API, and in particular if the two
|
||||
major versions coexist in the ecosystem for some time. In those cases, it can be
|
||||
motivated to have one `my-api-v2` and one `my-api-v3` named entity. This matches
|
||||
the end user's expectations when searching for the API, and matches the desire
|
||||
to maybe have separate documentation for the two and similar. But use this
|
||||
sparingly - only do it if the extra modelling burden is outweighed by any
|
||||
potential better clarity for users.
|
||||
|
||||
When writing your custom plugins, we encourage designing them such that they can
|
||||
show all the different variations through environments etc under one canonical
|
||||
reference to your software in the catalog. For example for a continuous
|
||||
deployment plugin, a user is likely to be greatly helped by being able to see
|
||||
the entity's versions deployed in all different environments next to each other
|
||||
in one view. That is also where they might be offered the ability to promote
|
||||
from one environment to the other, do rollbacks, see their relative performance
|
||||
metrics, and similar. This coherency and collection of tooling in one place is
|
||||
where something like Backstage can offer the most value and effectiveness of
|
||||
use. Splitting your entities apart into small islands makes this harder.
|
||||
|
||||
## Implementing custom model extensions
|
||||
|
||||
This section walks you through the steps involved extending the catalog model
|
||||
with a new Entity type.
|
||||
|
||||
### Creating a custom entity definition
|
||||
|
||||
The first step of introducing a custom entity is to define what shape and schema
|
||||
it has. We do this using a TypeScript type, as well as a JSONSchema schema.
|
||||
|
||||
Most of the time you will want to have at least the TypeScript type of your
|
||||
extension available in both frontend and backend code, which means you likely
|
||||
want to have an isomorphic package that houses these types. Within the Backstage
|
||||
main repo the package naming pattern of `<plugin>-common` is used for isomorphic
|
||||
packages, and you may choose to adopt this pattern as well.
|
||||
|
||||
You can generate an isomorphic plugin package by running:`yarn new --select plugin-common`
|
||||
or you can run `yarn new` and then select "plugin-common" from the list of options
|
||||
|
||||
There's at this point no existing templates for generating isomorphic plugins
|
||||
using the `@backstage/cli`. Perhaps the simplest way to get started right now is
|
||||
to copy the contents of one of the existing packages in the main repository,
|
||||
such as `plugins/scaffolder-common`, and rename the folder and file contents to
|
||||
the desired name. This example uses _foobar_ as the plugin name so the plugin
|
||||
will be named _foobar-common_.
|
||||
|
||||
Once you have a common package in place you can start adding your own entity
|
||||
definitions. For the exact details on how to do that we defer to getting
|
||||
inspired by the existing
|
||||
[scaffolder-common](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-common/src/index.ts)
|
||||
package. But in short you will need to declare a TypeScript type and a
|
||||
JSONSchema for the new entity kind.
|
||||
|
||||
### Building a custom processor for the entity
|
||||
|
||||
The next step is to create a custom processor for your new entity kind. This
|
||||
will be used within the catalog to make sure that it's able to ingest and
|
||||
validate entities of our new kind. Just like with the definition package, you
|
||||
can find inspiration in for example the existing
|
||||
[ScaffolderEntitiesProcessor](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-scaffolder-entity-model/src/processor/ScaffolderEntitiesProcessor.ts).
|
||||
We also provide a high-level example of what a catalog process for a custom
|
||||
entity might look like:
|
||||
|
||||
```ts
|
||||
import { CatalogProcessor, CatalogProcessorEmit, processingResult } from '@backstage/plugin-catalog-node';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-common'
|
||||
import { Entity, entityKindSchemaValidator } from '@backstage/catalog-model';
|
||||
|
||||
// For an example of the JSONSchema format and how to use $ref markers to the
|
||||
// base definitions, see:
|
||||
// https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema/kinds/Component.v1alpha1.schema.json
|
||||
import { foobarEntityV1alpha1Schema } from '@internal/catalog-model';
|
||||
|
||||
export class FoobarEntitiesProcessor implements CatalogProcessor {
|
||||
// You often end up wanting to support multiple versions of your kind as you
|
||||
// iterate on the definition, so we keep each version inside this array as a
|
||||
// convenient pattern.
|
||||
private readonly validators = [
|
||||
// This is where we use the JSONSchema that we export from our isomorphic
|
||||
// package
|
||||
entityKindSchemaValidator(foobarEntityV1alpha1Schema),
|
||||
];
|
||||
|
||||
// Return processor name
|
||||
getProcessorName(): string {
|
||||
return 'FoobarEntitiesProcessor'
|
||||
}
|
||||
|
||||
// validateEntityKind is responsible for signaling to the catalog processing
|
||||
// engine that this entity is valid and should therefore be submitted for
|
||||
// further processing.
|
||||
async validateEntityKind(entity: Entity): Promise<boolean> {
|
||||
for (const validator of this.validators) {
|
||||
// If the validator throws an exception, the entity will be marked as
|
||||
// invalid.
|
||||
if (validator(entity)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Returning false signals that we don't know what this is, passing the
|
||||
// responsibility to other processors to try to validate it instead.
|
||||
return false;
|
||||
}
|
||||
|
||||
async postProcessEntity(
|
||||
entity: Entity,
|
||||
_location: LocationSpec,
|
||||
emit: CatalogProcessorEmit,
|
||||
): Promise<Entity> {
|
||||
if (
|
||||
entity.apiVersion === 'example.com/v1alpha1' &&
|
||||
entity.kind === 'Foobar'
|
||||
) {
|
||||
const foobarEntity = entity as FoobarEntityV1alpha1;
|
||||
|
||||
// Typically you will want to emit any relations associated with the
|
||||
// entity here.
|
||||
emit(processingResult.relation({ ... }))
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once the processor is created it can be wired up to the catalog via the
|
||||
`CatalogBuilder` in `packages/backend/src/plugins/catalog.ts`:
|
||||
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { FoobarEntitiesProcessor } from '@internal/plugin-foobar-backend';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-next-line */
|
||||
builder.addProcessor(new FoobarEntitiesProcessor());
|
||||
const { processingEngine, router } = await builder.build();
|
||||
// ..
|
||||
}
|
||||
```
|
||||
@@ -519,6 +519,9 @@ will be used within the catalog to make sure that it's able to ingest and
|
||||
validate entities of our new kind. Just like with the definition package, you
|
||||
can find inspiration in for example the existing
|
||||
[ScaffolderEntitiesProcessor](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-scaffolder-entity-model/src/processor/ScaffolderEntitiesProcessor.ts).
|
||||
|
||||
The custom processor should be created as a separate module for the catalog plugin. For information on how to set that up, see the [plugin docs](../../plugins/backend-plugin.md#creating-a-backend-plugin). Use `yarn new --select backend-module` instead to create a module. For our case, the module ID will be `foobar` and the plugin ID will be `catalog`.
|
||||
|
||||
We also provide a high-level example of what a catalog process for a custom
|
||||
entity might look like:
|
||||
|
||||
@@ -585,20 +588,43 @@ export class FoobarEntitiesProcessor implements CatalogProcessor {
|
||||
}
|
||||
```
|
||||
|
||||
Once the processor is created it can be wired up to the catalog via the
|
||||
`CatalogBuilder` in `packages/backend/src/plugins/catalog.ts`:
|
||||
#### New Backend
|
||||
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
To use your custom processor, you'll need to add the module to your backend as well as integrate your module with the catalog plugin.
|
||||
|
||||
```ts title="plugins/catalog-backend-module-foobar/src/index.ts"
|
||||
import {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
|
||||
/* highlight-add-next-line */
|
||||
import { FoobarEntitiesProcessor } from '@internal/plugin-foobar-backend';
|
||||
import { FoobarEntitiesProcessor } from './providers';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-next-line */
|
||||
builder.addProcessor(new FoobarEntitiesProcessor());
|
||||
const { processingEngine, router } = await builder.build();
|
||||
// ..
|
||||
}
|
||||
export const catalogModuleFoobarEntitiesProcessor = createBackendModule({
|
||||
pluginId: 'catalog',
|
||||
moduleId: 'foobar',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogProcessingExtensionPoint,
|
||||
},
|
||||
async init({ catalog }) {
|
||||
catalog.addProcessor(new FoobarEntitiesProcessor());
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default catalogModuleFoobarEntitiesProcessor;
|
||||
```
|
||||
|
||||
This module can then be installed to your backend like so,
|
||||
|
||||
```ts
|
||||
backend.add(import('@internal/plugin-catalog-backend-module-foobar'));
|
||||
```
|
||||
|
||||
#### Legacy Backend
|
||||
|
||||
Look through the [legacy documentation](./extending-the-model--old.md).
|
||||
|
||||
@@ -162,9 +162,13 @@ steps and merging them into the final object which is what is visible from the
|
||||
catalog API. As the final entity itself gets updated, the stitcher makes sure
|
||||
that the search table gets refreshed accordingly as well.
|
||||
|
||||
> Note: The search table mentioned here is not related to the core Search
|
||||
> feature of Backstage. It's rather the table that backs the ability to filter
|
||||
> catalog API query results.
|
||||
:::note Note
|
||||
|
||||
The search table mentioned here is not related to the core Search
|
||||
feature of Backstage. It's rather the table that backs the ability to filter
|
||||
catalog API query results.
|
||||
|
||||
:::
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -86,12 +86,17 @@ contains more information about the required fields.
|
||||
Once we have a `template.yaml` ready, we can then add it to the software catalog
|
||||
for use by the scaffolder.
|
||||
|
||||
> Note: When you add or modify a template, you will need to refresh the location entity.
|
||||
> Otherwise, Backstage won't display the template in the available templates,
|
||||
> or it will keep showing the old template. You can refresh the location instance by
|
||||
> going into `Catalog` web page, choosing `Locations` instead of `Components`, and selecting the correct location entity.
|
||||
> From there, you can click on the refresh icon representing "Scheduled entity refresh" action.
|
||||
> Afterwards, you should see your template updated.
|
||||
:::note Note
|
||||
|
||||
When you add or modify a template, you will need to refresh the location entity.
|
||||
Otherwise, Backstage won't display the template in the available templates,
|
||||
or it will keep showing the old template. You can refresh the location instance by
|
||||
going into `Catalog` web page, choosing `Locations` instead of `Components`, and selecting the correct
|
||||
location entity.
|
||||
From there, you can click on the refresh icon representing "Scheduled entity refresh" action.
|
||||
Afterwards, you should see your template updated.
|
||||
|
||||
:::
|
||||
|
||||
You can add the template files to the catalog through
|
||||
[static location configuration](../software-catalog/configuration.md#static-location-configuration),
|
||||
|
||||
+5
-1
@@ -286,4 +286,8 @@ backend.add(customPermissionBackendModule);
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
> Note: the `ExamplePermissionPolicy` here could be the one from the [Authorizing parameters and steps](#authorizing-parameters-and-steps) example or from the [Authorizing actions](#authorizing-actions) example. It would work the same way for both of them.
|
||||
:::note Note
|
||||
|
||||
The `ExamplePermissionPolicy` here could be the one from the [Authorizing parameters and steps](#authorizing-parameters-and-steps) example or from the [Authorizing actions](#authorizing-actions) example. It would work the same way for both of them.
|
||||
|
||||
:::
|
||||
|
||||
@@ -57,7 +57,11 @@ backend.add(import('@backstage/plugin-scaffolder-backend-module-github'));
|
||||
backend.start();
|
||||
```
|
||||
|
||||
> Note: This is a simplified example of what your backend may look like, you may have more code in here then this.
|
||||
:::note Note
|
||||
|
||||
This is a simplified example of what your backend may look like, you may have more code in here then this.
|
||||
|
||||
:::
|
||||
|
||||
## Listing Actions
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ This is done in your `app-config.yaml` by adding
|
||||
[Backstage integrations](https://backstage.io/docs/integrations/) for the
|
||||
appropriate source code repository for your organization.
|
||||
|
||||
> Note: Integrations may already be set up as part of your `app-config.yaml`.
|
||||
:::note Note
|
||||
|
||||
Integrations may already be set up as part of your `app-config.yaml`.
|
||||
|
||||
:::
|
||||
|
||||
The next step is to [add templates](http://backstage.io/docs/features/software-templates/adding-templates)
|
||||
to your Backstage app.
|
||||
|
||||
@@ -20,10 +20,14 @@ locations like GitHub or GitLab.
|
||||
> Be sure to have covered
|
||||
> [Getting Started with Backstage](../../getting-started) before proceeding.
|
||||
|
||||
> Note: if you're running Backstage with Node 20 or later, you'll need to pass the flag `--no-node-snapshot` to Node in order to
|
||||
> use the templates feature.
|
||||
> One way to do this is to specify the `NODE_OPTIONS` environment variable before starting Backstage:
|
||||
> `export NODE_OPTIONS=--no-node-snapshot`
|
||||
:::note Note
|
||||
|
||||
If you're running Backstage with Node 20 or later, you'll need to pass the flag `--no-node-snapshot` to Node in order to
|
||||
use the templates feature.
|
||||
One way to do this is to specify the `NODE_OPTIONS` environment variable before starting Backstage:
|
||||
`export NODE_OPTIONS=--no-node-snapshot`
|
||||
|
||||
:::
|
||||
|
||||
The Software Templates are available under `/create`. For local development you
|
||||
should be able to reach them at `http://localhost:3000/create`.
|
||||
|
||||
@@ -5,7 +5,11 @@ title: 'Migrating to react-jsonschema-form@v5'
|
||||
description: Docs on migrating to `react-jsonschema-form`@v5 and the new designs
|
||||
---
|
||||
|
||||
> Note: If you were previously using the `/alpha` imports to test out the `scaffolder/next` work, those imports have been promoted to the default exports from the respective packages. You should just have to remove the `/alpha` from the import path, and remove the `Next` from the import name. `NextScaffolderPage` -> `ScaffolderPage`, `createNextScaffolderFieldExtension` -> `createScaffolderFieldExtension` etc.
|
||||
:::note Note
|
||||
|
||||
If you were previously using the `/alpha` imports to test out the `scaffolder/next` work, those imports have been promoted to the default exports from the respective packages. You should just have to remove the `/alpha` from the import path, and remove the `Next` from the import name. `NextScaffolderPage` -> `ScaffolderPage`, `createNextScaffolderFieldExtension` -> `createScaffolderFieldExtension` etc.
|
||||
|
||||
:::
|
||||
|
||||
## What's `react-jsonschema-form`?
|
||||
|
||||
|
||||
@@ -8,10 +8,14 @@ If you want to extend the functionality of the Scaffolder, you can do so
|
||||
by writing custom actions which can be used alongside our
|
||||
[built-in actions](./builtin-actions.md).
|
||||
|
||||
> Note: When adding custom actions, the actions array will **replace the
|
||||
> built-in actions too**. Meaning, you will no longer be able to use them.
|
||||
> If you want to continue using the builtin actions, include them in the actions
|
||||
> array when registering your custom actions, as seen below.
|
||||
:::note Note
|
||||
|
||||
When adding custom actions, the actions array will **replace the
|
||||
built-in actions too**. Meaning, you will no longer be able to use them.
|
||||
If you want to continue using the builtin actions, include them in the actions
|
||||
array when registering your custom actions, as seen below.
|
||||
|
||||
:::
|
||||
|
||||
## Writing your Custom Action
|
||||
|
||||
|
||||
@@ -13,9 +13,14 @@ out-of-the box experience.
|
||||
|
||||

|
||||
|
||||
> Note: See below for our recommended deployment architecture which takes care
|
||||
> of stability, scalability and speed. Also look at the
|
||||
> [HOW TO migrate guide](how-to-guides.md#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach).
|
||||
:::note Note
|
||||
|
||||
See below for our recommended deployment architecture which takes care
|
||||
of stability, scalability and speed. Also look at the
|
||||
[HOW TO migrate guide](how-to-guides
|
||||
md#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach).
|
||||
|
||||
:::
|
||||
|
||||
When you open a TechDocs site in Backstage, the
|
||||
[TechDocs Reader](./concepts.md#techdocs-reader) makes a request to
|
||||
|
||||
@@ -219,7 +219,11 @@ backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
|
||||
backend.start();
|
||||
```
|
||||
|
||||
> Note: The above is a very simplified example, you may have more content then this in your version.
|
||||
:::note Note
|
||||
|
||||
The above is a very simplified example, you may have more content then this in your version.
|
||||
|
||||
:::
|
||||
|
||||
## Setting the configuration
|
||||
|
||||
|
||||
@@ -499,8 +499,12 @@ Start writing your documentation by adding more markdown (.md) files to this
|
||||
folder (/docs) or replace the content in this file.
|
||||
```
|
||||
|
||||
> Note: The values of `site_name`, `component_id` and `site_description` depends
|
||||
> on how you have configured your `template.yaml`
|
||||
:::note Note
|
||||
|
||||
The values of `site_name`, `component_id` and `site_description` depends
|
||||
on how you have configured your `template.yaml`.
|
||||
|
||||
:::
|
||||
|
||||
Done! You now have support for TechDocs in your own software template!
|
||||
|
||||
@@ -514,7 +518,11 @@ theme:
|
||||
font: false
|
||||
```
|
||||
|
||||
> Note: The addition `name: material` is necessary. Otherwise it will not work
|
||||
:::note Note
|
||||
|
||||
The addition `name: material` is necessary. Otherwise it will not work
|
||||
|
||||
:::
|
||||
|
||||
## How to enable iframes in TechDocs
|
||||
|
||||
@@ -623,12 +631,16 @@ plugins:
|
||||
- kroki
|
||||
```
|
||||
|
||||
> Note: you will very likely want to set a `kroki` `ServerURL` configuration in your
|
||||
> `mkdocs.yml` as well. The default value is the publicly hosted `kroki.io`. If
|
||||
> you have sensitive information in your organization's diagrams, you should set
|
||||
> up a [server of your own](https://docs.kroki.io/kroki/setup/install/) and use it
|
||||
> instead. Check out [mkdocs-kroki-plugin config](https://github.com/AVATEAM-IT-SYSTEMHAUS/mkdocs-kroki-plugin#config)
|
||||
> for more plugin configuration details.
|
||||
:::note Note
|
||||
|
||||
You will very likely want to set a `kroki` `ServerURL` configuration in your
|
||||
`mkdocs.yml` as well. The default value is the publicly hosted `kroki.io`. If
|
||||
you have sensitive information in your organization's diagrams, you should set
|
||||
up a [server of your own](https://docs.kroki.io/kroki/setup/install/) and use it
|
||||
instead. Check out [mkdocs-kroki-plugin config](https://github.com/AVATEAM-IT-SYSTEMHAUS/mkdocs-kroki-plugin#config)
|
||||
for more plugin configuration details.
|
||||
|
||||
:::
|
||||
|
||||
4. **Add mermaid code into TechDocs:**
|
||||
|
||||
@@ -766,7 +778,11 @@ backend.add(techdocsCustomBuildStrategy());
|
||||
backend.start();
|
||||
```
|
||||
|
||||
> Note: You may need to add the `@backstage/plugin-techdocs-node` package to your backend `package.json` if it's not been imported already.
|
||||
:::note Note
|
||||
|
||||
You may need to add the `@backstage/plugin-techdocs-node` package to your backend `package.json` if it's not been imported already.
|
||||
|
||||
:::
|
||||
|
||||
## How to use other mkdocs plugins?
|
||||
|
||||
|
||||
@@ -169,17 +169,21 @@ permissions to:
|
||||
- `s3:ListBucket` - To retrieve bucket metadata
|
||||
- `s3:GetObject` - To retrieve files from the bucket
|
||||
|
||||
> Note: If you need to migrate documentation objects from an older-style path
|
||||
> format including case-sensitive entity metadata, you will need to add some
|
||||
> additional permissions to be able to perform the migration, including:
|
||||
>
|
||||
> - `s3:PutBucketAcl` (for copying files,
|
||||
> [more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html))
|
||||
> - `s3:DeleteObject` and `s3:DeleteObjectVersion` (for deleting migrated files,
|
||||
> [more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html))
|
||||
>
|
||||
> ...And you will need to ensure the permissions apply to the bucket itself, as
|
||||
> well as all resources under the bucket. See the example policy below.
|
||||
:::note Note
|
||||
|
||||
If you need to migrate documentation objects from an older-style path
|
||||
format including case-sensitive entity metadata, you will need to add some
|
||||
additional permissions to be able to perform the migration, including:
|
||||
|
||||
- `s3:PutBucketAcl` (for copying files,
|
||||
[more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html))
|
||||
- `s3:DeleteObject` and `s3:DeleteObjectVersion` (for deleting migrated files,
|
||||
[more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html))
|
||||
|
||||
...And you will need to ensure the permissions apply to the bucket itself, as
|
||||
well as all resources under the bucket. See the example policy below.
|
||||
|
||||
:::
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -68,11 +68,15 @@ integrations:
|
||||
|
||||
If you do not specify the `organizations` field the credential will be used for all organizations for which no other credential is configured.
|
||||
|
||||
> Note: An Azure DevOps provider is added automatically at startup for
|
||||
> convenience, so you only need to list it if you want to supply a
|
||||
> [personalAccessToken](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate),
|
||||
> a [service principal](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity),
|
||||
> or a [managed identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity)
|
||||
:::note Note
|
||||
|
||||
An Azure DevOps provider is added automatically at startup for
|
||||
convenience, so you only need to list it if you want to supply a
|
||||
[personalAccessToken](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate),
|
||||
a [service principal](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity),
|
||||
or a [managed identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity)
|
||||
|
||||
:::
|
||||
|
||||
The configuration is a structure with these elements:
|
||||
|
||||
@@ -86,9 +90,11 @@ The `credentials` element is a structure with these elements:
|
||||
- `tenantId`: The tenant ID of the service principal (required for service principal)
|
||||
- `personalAccessToken`: The personal access token (required for personal access token)
|
||||
|
||||
> Note:
|
||||
>
|
||||
> - You cannot use a service principal or managed identity for Azure DevOps Server (on-premises) organizations
|
||||
> - You can only use a service principal or managed identity for Microsoft Entra ID (formerly Azure Active Directory) backed Azure DevOps organizations
|
||||
> - You can only specify one credential per host without any organizations specified
|
||||
> - The personal access token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API
|
||||
:::note Note
|
||||
|
||||
- You cannot use a service principal or managed identity for Azure DevOps Server (on-premises) organizations
|
||||
- You can only use a service principal or managed identity for Microsoft Entra ID (formerly Azure Active Directory) backed Azure DevOps organizations
|
||||
- You can only specify one credential per host without any organizations specified
|
||||
- The personal access token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API
|
||||
|
||||
:::
|
||||
|
||||
@@ -22,10 +22,18 @@ integrations:
|
||||
appPassword: ${BITBUCKET_CLOUD_PASSWORD}
|
||||
```
|
||||
|
||||
> Note: A public Bitbucket Cloud provider is added automatically at startup for
|
||||
> convenience, so you only need to list it if you want to supply credentials.
|
||||
:::note Note
|
||||
|
||||
> Note: The credential used for this is type [App Password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/). An Atlassian Account API key will not work
|
||||
A public Bitbucket Cloud provider is added automatically at startup for
|
||||
convenience, so you only need to list it if you want to supply credentials.
|
||||
|
||||
:::
|
||||
|
||||
:::note Note
|
||||
|
||||
The credential used for this is type [App Password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/). An Atlassian Account API key will not work.
|
||||
|
||||
:::
|
||||
|
||||
Directly under the `bitbucketCloud` key is a list of provider configurations, where
|
||||
you can list the Bitbucket Cloud providers you want to fetch data from.
|
||||
|
||||
@@ -130,8 +130,12 @@ catalog:
|
||||
|
||||
This provider supports multiple organizations via unique provider IDs.
|
||||
|
||||
> **Note:** It is possible but certainly not recommended to skip the provider ID level.
|
||||
> If you do so, `default` will be used as provider ID.
|
||||
:::note Note
|
||||
|
||||
It is possible but certainly not recommended to skip the provider ID level.
|
||||
If you do so, `default` will be used as provider ID.
|
||||
|
||||
:::
|
||||
|
||||
- **`catalogPath`** _(optional)_:
|
||||
Default: `/catalog-info.yaml`.
|
||||
|
||||
@@ -17,9 +17,13 @@ is a hierarchy of
|
||||
[`Group`](../../features/software-catalog/descriptor-format.md#kind-group) kind
|
||||
entities that mirror your org setup.
|
||||
|
||||
> Note: This adds `User` and `Group` entities to the catalog, but does not
|
||||
> provide authentication. See the
|
||||
> [GitHub auth provider](../../auth/github/provider.md) for that.
|
||||
:::note Note
|
||||
|
||||
This adds `User` and `Group` entities to the catalog, but does not
|
||||
provide authentication. See the
|
||||
[GitHub auth provider](../../auth/github/provider.md) for that.
|
||||
|
||||
:::
|
||||
|
||||
## Permissions
|
||||
|
||||
|
||||
@@ -136,7 +136,11 @@ To use the discovery provider, you'll need a GitLab integration
|
||||
[set up](locations.md) with a `token`. Then you can add a provider config per group
|
||||
to the catalog configuration.
|
||||
|
||||
> > NOTE: if you are using the New Backend System, the `schedule` has to be setup in the config, as shown below.
|
||||
:::note Note
|
||||
|
||||
If you are using the New Backend System, the `schedule` has to be setup in the config, as shown below.
|
||||
|
||||
:::
|
||||
|
||||
```yaml title="app-config.yaml"
|
||||
catalog:
|
||||
|
||||
@@ -158,7 +158,11 @@ amount of data, this can take significant time and resources.
|
||||
The token used must have the `read_api` scope, and the Users and Groups fetched
|
||||
will be those visible to the account which provisioned the token.
|
||||
|
||||
> > NOTE: if you are using the New Backend System, the `schedule` has to be setup in the config, as shown below.
|
||||
:::note Note
|
||||
|
||||
If you are using the New Backend System, the `schedule` has to be setup in the config, as shown below.
|
||||
|
||||
:::
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
|
||||
@@ -29,8 +29,12 @@ to `@backstage/plugin-catalog-backend-module-ldap` to your backend package.
|
||||
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-ldap
|
||||
```
|
||||
|
||||
> Note: When configuring to use a Provider instead of a Processor you do not
|
||||
> need to add a _location_ pointing to your LDAP server
|
||||
:::note Note
|
||||
|
||||
When configuring to use a Provider instead of a Processor you do not
|
||||
need to add a _location_ pointing to your LDAP server
|
||||
|
||||
:::
|
||||
|
||||
Update the catalog plugin initialization in your backend to add the provider and
|
||||
schedule it:
|
||||
|
||||
@@ -8,7 +8,11 @@ If you prefer to watch a video instead, you can start with this video introducti
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/EQr9tFClgG0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
> Note: This video was recorded in the January 2022 Contributors Session using `@backstage/create-app@0.4.14`. Some aspects of the demo may have changed in later releases.
|
||||
:::note Note
|
||||
|
||||
This video was recorded in the January 2022 Contributors Session using `@backstage/create-app@0.4.14`. Some aspects of the demo may have changed in later releases.
|
||||
|
||||
:::
|
||||
|
||||
Backstage integrators control permissions by writing a policy. In general terms, a policy is simply an async function which receives a request to authorize a specific action for a user and (optional) resource, and returns a decision on whether to authorize that permission. Integrators can implement their own policies from scratch, or adopt reusable policies written by others.
|
||||
|
||||
|
||||
@@ -36,7 +36,11 @@ This approach will work for simple cases, but it has a downside: it forces us to
|
||||
|
||||
To avoid this situation, the permissions framework has support for filtering items in the data source itself. In this part of the tutorial, we'll describe the steps required to use that behavior.
|
||||
|
||||
> Note: in order to perform authorization filtering in this way, the data source must allow filters to be logically combined with AND, OR, and NOT operators. The conditional decisions returned by the permissions framework use a [nested object](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) to combine conditions. If you're implementing a filter API from scratch, we recommend using the same shape for ease of interoperability. If not, you'll need to implement a function which transforms the nested object into your own format.
|
||||
:::note Note
|
||||
|
||||
In order to perform authorization filtering in this way, the data source must allow filters to be logically combined with AND, OR, and NOT operators. The conditional decisions returned by the permissions framework use a [nested object](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) to combine conditions. If you're implementing a filter API from scratch, we recommend using the same shape for ease of interoperability. If not, you'll need to implement a function which transforms the nested object into your own format.
|
||||
|
||||
:::
|
||||
|
||||
## Creating the read permission
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ In the previous sections, we learned how to protect our plugin's backend API rou
|
||||
|
||||
Take, for example, the "Add" button in our todo list application. When a user clicks this button, the frontend makes a `POST` request to the `/todos` route of our backend. If a user tries to add a todo but is not authorized, they will have no way of knowing this until they perform the action and are faced with an error. This is a poor user experience. We can do better by disabling the add button.
|
||||
|
||||
> Note: Placing frontend components behind authorization cannot take the place of placing your backend routes behind authorization. Authorization checks on the frontend should be used in _addition_ to the corresponding backend authorization, as an improvement to the user experience. If you do not place your backend route behind authorization, a malicious actor can still send a request to the route even if you disabled the corresponding frontend component.
|
||||
:::note Note
|
||||
|
||||
Placing frontend components behind authorization cannot take the place of placing your backend routes behind authorization. Authorization checks on the frontend should be used in _addition_ to the corresponding backend authorization, as an improvement to the user experience. If you do not place your backend route behind authorization, a malicious actor can still send a request to the route even if you disabled the corresponding frontend component.
|
||||
|
||||
:::
|
||||
|
||||
## Using `usePermission`
|
||||
|
||||
|
||||
@@ -28,12 +28,7 @@ backend.add(import('@backstage/plugin-proxy-backend/alpha'));
|
||||
In `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.loadConfig(configReader)
|
||||
/** ... other routers ... */
|
||||
.addRouter('/proxy', await proxy(proxyEnv));
|
||||
backend.add(import('@backstage/plugin-proxy-backend/alpha'));
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -50,6 +45,7 @@ proxy:
|
||||
/simple-example: http://simple.example.com:8080
|
||||
'/larger-example/v1':
|
||||
target: http://larger.example.com:8080/svc.v1
|
||||
credentials: require
|
||||
headers:
|
||||
Authorization: ${EXAMPLE_AUTH_HEADER}
|
||||
# ...or interpolating a value into part of a string,
|
||||
@@ -66,6 +62,23 @@ backend requests to `/api/proxy/simple-example/...` and
|
||||
The value inside each route is either a simple URL string, or an object on the
|
||||
format accepted by
|
||||
[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware).
|
||||
Additionally, it has an optional `credentials` key which can have the following
|
||||
values:
|
||||
|
||||
- `require`: Callers must provide Backstage user or service credentials with
|
||||
each request. The credentials are not forwarded to the proxy target. This is
|
||||
the default.
|
||||
- `forward`: Callers must provide Backstage user or service credentials with
|
||||
each request, and those credentials are forwarded to the proxy target.
|
||||
- `dangerously-allow-unauthenticated`: No Backstage credentials are required to
|
||||
access this proxy target. The target can still apply its own credentials
|
||||
checks, but the proxy will not help block non-Backstage-blessed callers. If
|
||||
you also add `allowedHeaders: ['Authorization']` to an endpoint configuration,
|
||||
then the Backstage token (if provided) WILL be forwarded.
|
||||
|
||||
Note that if you have `backend.auth.dangerouslyDisableDefaultAuthPolicy` set to
|
||||
`true`, the `credentials` value does not apply; the proxy will behave as if all
|
||||
endpoints were set to `dangerously-allow-unauthenticated`.
|
||||
|
||||
If the value is a string, it is assumed to correspond to:
|
||||
|
||||
@@ -74,6 +87,7 @@ target: <the string>
|
||||
changeOrigin: true
|
||||
pathRewrite:
|
||||
'^<url prefix><the string>/': '/'
|
||||
credentials: require
|
||||
```
|
||||
|
||||
When the target is an object, it is given verbatim to `http-proxy-middleware`
|
||||
@@ -86,6 +100,7 @@ except with the following caveats for convenience:
|
||||
`'^/api/proxy/larger-example/v1/': '/'` is added. That means that a request to
|
||||
`/api/proxy/larger-example/v1/some/path` will be translated to a request to
|
||||
`http://larger.example.com:8080/svc.v1/some/path`.
|
||||
- If `credentials` is not specified, it is set to `require`.
|
||||
|
||||
There are also additional settings:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Apollo Explorer
|
||||
author: unredundant
|
||||
authorUrl: https://github.com/unredundant
|
||||
author: brizzbuzz
|
||||
authorUrl: https://github.com/brizzbuzz
|
||||
category: Debugging
|
||||
description: Integrates Apollo Explorer graphs as a tool to browse GraphQL API endpoints inside Backstage.
|
||||
documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/apollo-explorer/plugins/apollo-explorer/README.md
|
||||
|
||||
@@ -5,7 +5,7 @@ authorUrl: https://croz.net/?utm_source=backstage.io&utm_medium=marketplace&utm_
|
||||
category: API Management
|
||||
description: Bring IBM APIC to Backstage.
|
||||
documentation: https://github.com/croz-ltd/apic-backend-plugin?utm_source=backstage.io&utm_medium=marketplace&utm_campaign=backstage-ibm-apic-backend
|
||||
iconUrl: https://croz.net/wp-content/uploads/2024/02/croz_large-o.png
|
||||
iconUrl: https://croz.net/app/uploads/2024/05/apple-touch-icon.png
|
||||
npmPackageName: '@croz/plugin-ibm-apic-backend'
|
||||
tags:
|
||||
- openapic
|
||||
|
||||
@@ -181,6 +181,7 @@ const config: Config = {
|
||||
hideIcon: true,
|
||||
customFont: true,
|
||||
buttonStyle: 'dark',
|
||||
ratingMode: 'stars',
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
@@ -65,7 +65,7 @@ export interface Backend {
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export const cacheServiceFactory: () => ServiceFactory<CacheClient, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -100,7 +100,7 @@ export interface CreateSpecializedBackendOptions {
|
||||
defaultServiceFactories: ServiceFactoryOrFunction[];
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export const databaseServiceFactory: () => ServiceFactory<
|
||||
PluginDatabaseManager,
|
||||
'plugin'
|
||||
@@ -121,7 +121,7 @@ export interface DefaultRootHttpRouterOptions {
|
||||
indexPath?: string | false;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export const discoveryServiceFactory: () => ServiceFactory<
|
||||
DiscoveryService,
|
||||
'plugin'
|
||||
@@ -137,7 +137,7 @@ export interface ExtendedHttpServer extends http.Server {
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export class HostDiscovery implements DiscoveryService {
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
@@ -190,13 +190,13 @@ export type HttpServerOptions = {
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type IdentityFactoryOptions = {
|
||||
issuer?: string;
|
||||
algorithms?: string[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export const identityServiceFactory: (
|
||||
options?: IdentityFactoryOptions | undefined,
|
||||
) => ServiceFactory<IdentityService, 'plugin'>;
|
||||
@@ -208,7 +208,7 @@ export interface LifecycleMiddlewareOptions {
|
||||
startupRequestPauseTimeout?: HumanDuration;
|
||||
}
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export const lifecycleServiceFactory: () => ServiceFactory<
|
||||
LifecycleService,
|
||||
'plugin'
|
||||
@@ -255,7 +255,7 @@ export interface MiddlewareFactoryOptions {
|
||||
logger: LoggerService;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export const permissionsServiceFactory: () => ServiceFactory<
|
||||
PermissionsService,
|
||||
'plugin'
|
||||
@@ -270,7 +270,7 @@ export function readHelmetOptions(config?: Config): HelmetOptions;
|
||||
// @public
|
||||
export function readHttpServerOptions(config?: Config): HttpServerOptions;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export interface RootConfigFactoryOptions {
|
||||
argv?: string[];
|
||||
remote?: Pick<RemoteConfigSourceOptions, 'reloadInterval'>;
|
||||
@@ -278,7 +278,7 @@ export interface RootConfigFactoryOptions {
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export const rootConfigServiceFactory: (
|
||||
options?: RootConfigFactoryOptions | undefined,
|
||||
) => ServiceFactory<RootConfigService, 'root'>;
|
||||
@@ -314,7 +314,7 @@ export const rootHttpRouterServiceFactory: (
|
||||
options?: RootHttpRouterFactoryOptions | undefined,
|
||||
) => ServiceFactory<RootHttpRouterService, 'root'>;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export const rootLifecycleServiceFactory: () => ServiceFactory<
|
||||
RootLifecycleService,
|
||||
'root'
|
||||
@@ -326,19 +326,19 @@ export const rootLoggerServiceFactory: () => ServiceFactory<
|
||||
'root'
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export const schedulerServiceFactory: () => ServiceFactory<
|
||||
SchedulerService,
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export const tokenManagerServiceFactory: () => ServiceFactory<
|
||||
TokenManagerService,
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export const urlReaderServiceFactory: () => ServiceFactory<UrlReader, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('WinstonLogger', () => {
|
||||
expect.objectContaining({
|
||||
[MESSAGE]: JSON.stringify({
|
||||
level: 'error',
|
||||
message: '[REDACTED]) from this file',
|
||||
message: '***) from this file',
|
||||
}),
|
||||
}),
|
||||
expect.any(Function),
|
||||
@@ -83,10 +83,10 @@ describe('WinstonLogger', () => {
|
||||
[MESSAGE]: JSON.stringify({
|
||||
level: 'error',
|
||||
message: 'something went wrong',
|
||||
nested: '[REDACTED] (world) from nested object',
|
||||
nested: '*** (world) from nested object',
|
||||
null: null,
|
||||
nullProto: {
|
||||
foo: '[REDACTED] foo',
|
||||
foo: '*** foo',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -92,7 +92,7 @@ export class WinstonLogger implements RootLoggerService {
|
||||
return obj;
|
||||
}
|
||||
|
||||
obj[MESSAGE] = obj[MESSAGE]?.replace?.(redactionPattern, '[REDACTED]');
|
||||
obj[MESSAGE] = obj[MESSAGE]?.replace?.(redactionPattern, '***');
|
||||
|
||||
return obj;
|
||||
})(),
|
||||
|
||||
+4
-1
@@ -20,7 +20,10 @@ import {
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/cache` instead.
|
||||
*/
|
||||
export const cacheServiceFactory = createServiceFactory({
|
||||
service: coreServices.cache,
|
||||
deps: {
|
||||
|
||||
+8
-2
@@ -23,7 +23,10 @@ import {
|
||||
RemoteConfigSourceOptions,
|
||||
} from '@backstage/config-loader';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/rootConfig` instead.
|
||||
*/
|
||||
export interface RootConfigFactoryOptions {
|
||||
/**
|
||||
* Process arguments to use instead of the default `process.argv()`.
|
||||
@@ -37,7 +40,10 @@ export interface RootConfigFactoryOptions {
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/rootConfig` instead.
|
||||
*/
|
||||
export const rootConfigServiceFactory = createServiceFactory(
|
||||
(options?: RootConfigFactoryOptions) => ({
|
||||
service: coreServices.rootConfig,
|
||||
|
||||
+4
-1
@@ -21,7 +21,10 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/database` instead.
|
||||
*/
|
||||
export const databaseServiceFactory = createServiceFactory({
|
||||
service: coreServices.database,
|
||||
deps: {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './scheduler';
|
||||
@@ -29,6 +29,7 @@ type Target = string | { internal: string; external: string };
|
||||
* resolved to the same host, so there won't be any balancing of internal traffic.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/discovery` instead.
|
||||
*/
|
||||
export class HostDiscovery implements DiscoveryService {
|
||||
/**
|
||||
|
||||
+4
-1
@@ -20,7 +20,10 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { HostDiscovery } from './HostDiscovery';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/discovery` instead.
|
||||
*/
|
||||
export const discoveryServiceFactory = createServiceFactory({
|
||||
service: coreServices.discovery,
|
||||
deps: {
|
||||
|
||||
+9
-3
@@ -24,16 +24,22 @@ import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
* An identity client options object which allows extra configurations
|
||||
*
|
||||
* @public
|
||||
* @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead
|
||||
*/
|
||||
export type IdentityFactoryOptions = {
|
||||
issuer?: string;
|
||||
|
||||
/** JWS "alg" (Algorithm) Header Parameter values. Defaults to an array containing just ES256.
|
||||
* More info on supported algorithms: https://github.com/panva/jose */
|
||||
/**
|
||||
* JWS "alg" (Algorithm) Header Parameter values. Defaults to an array containing just ES256.
|
||||
* More info on supported algorithms: https://github.com/panva/jose
|
||||
*/
|
||||
algorithms?: string[];
|
||||
};
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead
|
||||
*/
|
||||
export const identityServiceFactory = createServiceFactory(
|
||||
(options?: IdentityFactoryOptions) => ({
|
||||
service: coreServices.identity,
|
||||
|
||||
@@ -28,7 +28,8 @@ export * from './permissions';
|
||||
export * from './rootHttpRouter';
|
||||
export * from './rootLifecycle';
|
||||
export * from './rootLogger';
|
||||
export * from './scheduler';
|
||||
export * from './tokenManager';
|
||||
export * from './urlReader';
|
||||
export * from './userInfo';
|
||||
|
||||
export * from './deprecated';
|
||||
|
||||
+6
-1
@@ -26,7 +26,10 @@ import {
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @internal */
|
||||
/**
|
||||
* @internal
|
||||
* @deprecated
|
||||
*/
|
||||
export class BackendPluginLifecycleImpl implements LifecycleService {
|
||||
constructor(
|
||||
private readonly logger: LoggerService,
|
||||
@@ -85,7 +88,9 @@ export class BackendPluginLifecycleImpl implements LifecycleService {
|
||||
|
||||
/**
|
||||
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/lifecycle` instead.
|
||||
*/
|
||||
export const lifecycleServiceFactory = createServiceFactory({
|
||||
service: coreServices.lifecycle,
|
||||
|
||||
+4
-1
@@ -20,7 +20,10 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/permissions` instead.
|
||||
*/
|
||||
export const permissionsServiceFactory = createServiceFactory({
|
||||
service: coreServices.permissions,
|
||||
deps: {
|
||||
|
||||
+5
-1
@@ -25,7 +25,10 @@ import {
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @internal */
|
||||
/**
|
||||
* @internal
|
||||
* @deprecated
|
||||
*/
|
||||
export class BackendLifecycleImpl implements RootLifecycleService {
|
||||
constructor(private readonly logger: LoggerService) {}
|
||||
|
||||
@@ -108,6 +111,7 @@ export class BackendLifecycleImpl implements RootLifecycleService {
|
||||
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/rootLifecycle` instead.
|
||||
*/
|
||||
export const rootLifecycleServiceFactory = createServiceFactory({
|
||||
service: coreServices.rootLifecycle,
|
||||
|
||||
+4
-1
@@ -20,7 +20,10 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { TaskScheduler } from '@backstage/backend-tasks';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/scheduler` instead.
|
||||
*/
|
||||
export const schedulerServiceFactory = createServiceFactory({
|
||||
service: coreServices.scheduler,
|
||||
deps: {
|
||||
|
||||
+4
-1
@@ -20,7 +20,10 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ServerTokenManager } from '@backstage/backend-common';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead
|
||||
*/
|
||||
export const tokenManagerServiceFactory = createServiceFactory({
|
||||
service: coreServices.tokenManager,
|
||||
deps: {
|
||||
|
||||
+4
-1
@@ -20,7 +20,10 @@ import {
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/urlReader` instead.
|
||||
*/
|
||||
export const urlReaderServiceFactory = createServiceFactory({
|
||||
service: coreServices.urlReader,
|
||||
deps: {
|
||||
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
coreServices,
|
||||
ServiceRef,
|
||||
ServiceFactory,
|
||||
LifecycleService,
|
||||
RootLifecycleService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleServiceFactory';
|
||||
import { BackendPluginLifecycleImpl } from '../services/implementations/lifecycle/lifecycleServiceFactory';
|
||||
import { ServiceOrExtensionPoint } from './types';
|
||||
// Direct internal import to avoid duplication
|
||||
// eslint-disable-next-line @backstage/no-forbidden-package-imports
|
||||
@@ -345,27 +345,42 @@ export class BackendInitializer {
|
||||
}
|
||||
|
||||
// Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this
|
||||
async #getRootLifecycleImpl(): Promise<BackendLifecycleImpl> {
|
||||
async #getRootLifecycleImpl(): Promise<
|
||||
RootLifecycleService & {
|
||||
startup(): Promise<void>;
|
||||
shutdown(): Promise<void>;
|
||||
}
|
||||
> {
|
||||
const lifecycleService = await this.#serviceRegistry.get(
|
||||
coreServices.rootLifecycle,
|
||||
'root',
|
||||
);
|
||||
if (lifecycleService instanceof BackendLifecycleImpl) {
|
||||
return lifecycleService;
|
||||
|
||||
const service = lifecycleService as any;
|
||||
if (
|
||||
service &&
|
||||
typeof service.startup === 'function' &&
|
||||
typeof service.shutdown === 'function'
|
||||
) {
|
||||
return service;
|
||||
}
|
||||
|
||||
throw new Error('Unexpected root lifecycle service implementation');
|
||||
}
|
||||
|
||||
async #getPluginLifecycleImpl(
|
||||
pluginId: string,
|
||||
): Promise<BackendPluginLifecycleImpl> {
|
||||
): Promise<LifecycleService & { startup(): Promise<void> }> {
|
||||
const lifecycleService = await this.#serviceRegistry.get(
|
||||
coreServices.lifecycle,
|
||||
pluginId,
|
||||
);
|
||||
if (lifecycleService instanceof BackendPluginLifecycleImpl) {
|
||||
return lifecycleService;
|
||||
|
||||
const service = lifecycleService as any;
|
||||
if (service && typeof service.startup === 'function') {
|
||||
return service;
|
||||
}
|
||||
|
||||
throw new Error('Unexpected plugin lifecycle service implementation');
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -191,6 +191,11 @@ export interface Config {
|
||||
connection: string;
|
||||
/** An optional default TTL (in milliseconds). */
|
||||
defaultTtl?: number;
|
||||
/**
|
||||
* Whether or not [useRedisSets](https://github.com/jaredwray/keyv/tree/main/packages/redis#useredissets) should be configured to this redis cache.
|
||||
* Defaults to true if unspecified.
|
||||
*/
|
||||
useRedisSets?: boolean;
|
||||
}
|
||||
| {
|
||||
store: 'memcache';
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
## API Report File for "@backstage/backend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { CacheClient } from '@backstage/backend-common';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export const cacheServiceFactory: () => ServiceFactory<CacheClient, 'plugin'>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
## API Report File for "@backstage/backend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export const databaseServiceFactory: () => ServiceFactory<
|
||||
PluginDatabaseManager,
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
## API Report File for "@backstage/backend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { Config } from '@backstage/config';
|
||||
import { DiscoveryService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export const discoveryServiceFactory: () => ServiceFactory<
|
||||
DiscoveryService,
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export class HostDiscovery implements DiscoveryService {
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options?: {
|
||||
basePath?: string;
|
||||
},
|
||||
): HostDiscovery;
|
||||
// (undocumented)
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
// (undocumented)
|
||||
getExternalBaseUrl(pluginId: string): Promise<string>;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
## API Report File for "@backstage/backend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { LifecycleService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public
|
||||
export const lifecycleServiceFactory: () => ServiceFactory<
|
||||
LifecycleService,
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
## API Report File for "@backstage/backend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { PermissionsService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export const permissionsServiceFactory: () => ServiceFactory<
|
||||
PermissionsService,
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
## API Report File for "@backstage/backend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { RemoteConfigSourceOptions } from '@backstage/config-loader';
|
||||
import { RootConfigService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RootConfigFactoryOptions {
|
||||
argv?: string[];
|
||||
remote?: Pick<RemoteConfigSourceOptions, 'reloadInterval'>;
|
||||
// (undocumented)
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const rootConfigServiceFactory: (
|
||||
options?: RootConfigFactoryOptions | undefined,
|
||||
) => ServiceFactory<RootConfigService, 'root'>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
## API Report File for "@backstage/backend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { RootLifecycleService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public
|
||||
export const rootLifecycleServiceFactory: () => ServiceFactory<
|
||||
RootLifecycleService,
|
||||
'root'
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
## API Report File for "@backstage/backend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { SchedulerService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public
|
||||
export const schedulerServiceFactory: () => ServiceFactory<
|
||||
SchedulerService,
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
## API Report File for "@backstage/backend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
|
||||
// @public (undocumented)
|
||||
export const urlReaderServiceFactory: () => ServiceFactory<UrlReader, 'plugin'>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
/**
|
||||
* Options used by the default discovery service.
|
||||
*/
|
||||
discovery?: {
|
||||
/**
|
||||
* A list of target baseUrls and the associated plugins.
|
||||
*/
|
||||
endpoints: Array<{
|
||||
/**
|
||||
* The target base URL to use for the plugin.
|
||||
*
|
||||
* Can be either a string or an object with internal and external keys.
|
||||
* Targets with `{{pluginId}}` or `{{ pluginId }} in the URL will be replaced with the plugin ID.
|
||||
*/
|
||||
target: string | { internal: string; external: string };
|
||||
/**
|
||||
* Array of plugins which use the target base URL.
|
||||
*/
|
||||
plugins: string[];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.up = async function up(knex) {
|
||||
//
|
||||
// tasks
|
||||
//
|
||||
await knex.schema.createTable('backstage_backend_tasks__tasks', table => {
|
||||
table.comment('Tasks used for scheduling work on multiple workers');
|
||||
table
|
||||
.string('id')
|
||||
.primary()
|
||||
.notNullable()
|
||||
.comment('The unique ID of this particular task');
|
||||
table
|
||||
.text('settings_json')
|
||||
.notNullable()
|
||||
.comment('JSON serialized object with properties for this task');
|
||||
table
|
||||
.dateTime('next_run_start_at')
|
||||
.notNullable()
|
||||
.comment('The next time that the task should be started');
|
||||
table
|
||||
.text('current_run_ticket')
|
||||
.nullable()
|
||||
.comment('A unique ticket for the current task run');
|
||||
table
|
||||
.dateTime('current_run_started_at')
|
||||
.nullable()
|
||||
.comment('The time that the current task run started');
|
||||
table
|
||||
.dateTime('current_run_expires_at')
|
||||
.nullable()
|
||||
.comment('The time that the current task run will time out');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.down = async function down(knex) {
|
||||
//
|
||||
// tasks
|
||||
//
|
||||
await knex.schema.dropTable('backstage_backend_tasks__tasks');
|
||||
};
|
||||
@@ -2,46 +2,108 @@
|
||||
"name": "@backstage/backend-defaults",
|
||||
"description": "Backend defaults used by Backstage backend apps",
|
||||
"version": "0.2.18",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/backend-defaults"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./cache": "./src/entrypoints/cache/index.ts",
|
||||
"./database": "./src/entrypoints/database/index.ts",
|
||||
"./discovery": "./src/entrypoints/discovery/index.ts",
|
||||
"./lifecycle": "./src/entrypoints/lifecycle/index.ts",
|
||||
"./permissions": "./src/entrypoints/permissions/index.ts",
|
||||
"./rootConfig": "./src/entrypoints/rootConfig/index.ts",
|
||||
"./rootLifecycle": "./src/entrypoints/rootLifecycle/index.ts",
|
||||
"./scheduler": "./src/entrypoints/scheduler/index.ts",
|
||||
"./urlReader": "./src/entrypoints/urlReader/index.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"cache": [
|
||||
"src/entrypoints/cache/index.ts"
|
||||
],
|
||||
"database": [
|
||||
"src/entrypoints/database/index.ts"
|
||||
],
|
||||
"discovery": [
|
||||
"src/entrypoints/discovery/index.ts"
|
||||
],
|
||||
"lifecycle": [
|
||||
"src/entrypoints/lifecycle/index.ts"
|
||||
],
|
||||
"permissions": [
|
||||
"src/entrypoints/permissions/index.ts"
|
||||
],
|
||||
"rootConfig": [
|
||||
"src/entrypoints/rootConfig/index.ts"
|
||||
],
|
||||
"rootLifecycle": [
|
||||
"src/entrypoints/rootLifecycle/index.ts"
|
||||
],
|
||||
"scheduler": [
|
||||
"src/entrypoints/scheduler/index.ts"
|
||||
],
|
||||
"urlReader": [
|
||||
"src/entrypoints/urlReader/index.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"config.d.ts",
|
||||
"dist",
|
||||
"migrations"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"clean": "backstage-cli package clean",
|
||||
"start": "backstage-cli package start"
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-app-api": "workspace:^",
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/plugin-events-node": "workspace:^"
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/config-loader": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-events-node": "workspace:^",
|
||||
"@backstage/plugin-permission-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@opentelemetry/api": "^1.3.0",
|
||||
"cron": "^3.0.0",
|
||||
"knex": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/cli": "workspace:^",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
|
||||
@@ -16,26 +16,26 @@
|
||||
|
||||
import {
|
||||
Backend,
|
||||
cacheServiceFactory,
|
||||
rootConfigServiceFactory,
|
||||
createSpecializedBackend,
|
||||
databaseServiceFactory,
|
||||
discoveryServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
rootHttpRouterServiceFactory,
|
||||
lifecycleServiceFactory,
|
||||
rootLifecycleServiceFactory,
|
||||
loggerServiceFactory,
|
||||
permissionsServiceFactory,
|
||||
rootLoggerServiceFactory,
|
||||
schedulerServiceFactory,
|
||||
tokenManagerServiceFactory,
|
||||
urlReaderServiceFactory,
|
||||
identityServiceFactory,
|
||||
authServiceFactory,
|
||||
createSpecializedBackend,
|
||||
httpAuthServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
identityServiceFactory,
|
||||
loggerServiceFactory,
|
||||
rootHttpRouterServiceFactory,
|
||||
rootLoggerServiceFactory,
|
||||
tokenManagerServiceFactory,
|
||||
userInfoServiceFactory,
|
||||
} from '@backstage/backend-app-api';
|
||||
import { cacheServiceFactory } from '@backstage/backend-defaults/cache';
|
||||
import { databaseServiceFactory } from '@backstage/backend-defaults/database';
|
||||
import { discoveryServiceFactory } from '@backstage/backend-defaults/discovery';
|
||||
import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
|
||||
import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions';
|
||||
import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig';
|
||||
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
|
||||
import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler';
|
||||
import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader';
|
||||
import { eventsServiceFactory } from '@backstage/plugin-events-node';
|
||||
|
||||
export const defaultServiceFactories = [
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CacheManager } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const cacheServiceFactory = createServiceFactory({
|
||||
service: coreServices.cache,
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
plugin: coreServices.pluginMetadata,
|
||||
},
|
||||
async createRootContext({ config }) {
|
||||
return CacheManager.fromConfig(config);
|
||||
},
|
||||
async factory({ plugin }, manager) {
|
||||
return manager.forPlugin(plugin.getId()).getClient();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { cacheServiceFactory } from './cacheServiceFactory';
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DatabaseManager } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const databaseServiceFactory = createServiceFactory({
|
||||
service: coreServices.database,
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
lifecycle: coreServices.lifecycle,
|
||||
pluginMetadata: coreServices.pluginMetadata,
|
||||
},
|
||||
async createRootContext({ config }) {
|
||||
return config.getOptional('backend.database')
|
||||
? DatabaseManager.fromConfig(config)
|
||||
: DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: { client: 'better-sqlite3', connection: ':memory:' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
async factory({ pluginMetadata, lifecycle }, databaseManager) {
|
||||
return databaseManager.forPlugin(pluginMetadata.getId(), {
|
||||
pluginMetadata,
|
||||
lifecycle,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { databaseServiceFactory } from './databaseServiceFactory';
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { HostDiscovery } from './HostDiscovery';
|
||||
|
||||
describe('HostDiscovery', () => {
|
||||
it('is created from config', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:80/api/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:40/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('strips trailing slashes in config', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40//',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:80/api/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:40/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('can configure the base path', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
}),
|
||||
{ basePath: '/service' },
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:80/service/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://localhost:40/service/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ listen: ':80' }, 'http://localhost:80'],
|
||||
[{ listen: ':40', https: true }, 'https://localhost:40'],
|
||||
[{ listen: '127.0.0.1:80' }, 'http://127.0.0.1:80'],
|
||||
[{ listen: '127.0.0.1:80', https: true }, 'https://127.0.0.1:80'],
|
||||
[{ listen: '0.0.0.0:40' }, 'http://127.0.0.1:40'],
|
||||
[{ listen: { port: 80 } }, 'http://localhost:80'],
|
||||
[{ listen: { port: 8000 } }, 'http://localhost:8000'],
|
||||
[{ listen: { port: 80, host: '0.0.0.0' } }, 'http://127.0.0.1:80'],
|
||||
[{ listen: { port: 80, host: '::' } }, 'http://localhost:80'],
|
||||
[{ listen: { port: 80, host: '::1' } }, 'http://[::1]:80'],
|
||||
[{ listen: { port: 90, host: '::2' }, https: true }, 'https://[::2]:90'],
|
||||
])('resolves internal baseUrl for %j as %s', async (config, expected) => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
...config,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
`${expected}/api/catalog`,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses plugin specific targets from config if provided', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: {
|
||||
internal: 'http://catalog-backend-internal:8080/api/catalog',
|
||||
external: 'http://catalog-backend-external:8080/api/catalog',
|
||||
},
|
||||
plugins: ['catalog'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://catalog-backend-internal:8080/api/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://catalog-backend-external:8080/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses a single target for internal and external for a plugin', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://catalog-backend:8080/api/catalog',
|
||||
plugins: ['catalog'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://catalog-backend:8080/api/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://catalog-backend:8080/api/catalog',
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults to the backend baseUrl when there is not an endpoint for a plugin', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://catalog-backend:8080/api/catalog',
|
||||
plugins: ['catalog'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe(
|
||||
'http://localhost:80/api/scaffolder',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe(
|
||||
'http://localhost:40/api/scaffolder',
|
||||
);
|
||||
});
|
||||
|
||||
it('replaces {{pluginId}} or {{ pluginId }} in the target', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://common-backend:8080/api/{{pluginId}}',
|
||||
plugins: ['catalog', 'docs'],
|
||||
},
|
||||
{
|
||||
target: {
|
||||
internal: 'http://scaffolder-internal:8080/api/{{ pluginId }}',
|
||||
external: 'http://scaffolder-external:8080/api/{{ pluginId }}',
|
||||
},
|
||||
plugins: ['scaffolder'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
|
||||
'http://common-backend:8080/api/catalog',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
|
||||
'http://common-backend:8080/api/catalog',
|
||||
);
|
||||
await expect(discovery.getBaseUrl('docs')).resolves.toBe(
|
||||
'http://common-backend:8080/api/docs',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('docs')).resolves.toBe(
|
||||
'http://common-backend:8080/api/docs',
|
||||
);
|
||||
await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe(
|
||||
'http://scaffolder-internal:8080/api/scaffolder',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe(
|
||||
'http://scaffolder-external:8080/api/scaffolder',
|
||||
);
|
||||
});
|
||||
|
||||
it('encodes the pluginId', async () => {
|
||||
const discovery = HostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:40',
|
||||
listen: { port: 80, host: 'localhost' },
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: 'http://common-backend:8080/api/{{pluginId}}',
|
||||
plugins: ['plugin/beta'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(discovery.getBaseUrl('plugin/beta')).resolves.toBe(
|
||||
'http://common-backend:8080/api/plugin%2Fbeta',
|
||||
);
|
||||
await expect(discovery.getBaseUrl('plugin/alpha')).resolves.toBe(
|
||||
'http://localhost:80/api/plugin%2Falpha',
|
||||
);
|
||||
await expect(discovery.getExternalBaseUrl('plugin/alpha')).resolves.toBe(
|
||||
'http://localhost:40/api/plugin%2Falpha',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { readHttpServerOptions } from '@backstage/backend-app-api';
|
||||
import { DiscoveryService } from '@backstage/backend-plugin-api';
|
||||
|
||||
type Target = string | { internal: string; external: string };
|
||||
|
||||
/**
|
||||
* HostDiscovery is a basic PluginEndpointDiscovery implementation
|
||||
* that can handle plugins that are hosted in a single or multiple deployments.
|
||||
*
|
||||
* The deployment may be scaled horizontally, as long as the external URL
|
||||
* is the same for all instances. However, internal URLs will always be
|
||||
* resolved to the same host, so there won't be any balancing of internal traffic.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class HostDiscovery implements DiscoveryService {
|
||||
/**
|
||||
* Creates a new HostDiscovery discovery instance by reading
|
||||
* from the `backend` config section, specifically the `.baseUrl` for
|
||||
* discovering the external URL, and the `.listen` and `.https` config
|
||||
* for the internal one.
|
||||
*
|
||||
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
|
||||
* eg.
|
||||
* ```yaml
|
||||
* discovery:
|
||||
* endpoints:
|
||||
* - target: https://internal.example.com/internal-catalog
|
||||
* plugins: [catalog]
|
||||
* - target: https://internal.example.com/secure/api/{{pluginId}}
|
||||
* plugins: [auth, permission]
|
||||
* - target:
|
||||
* internal: https://internal.example.com/search
|
||||
* external: https://example.com/search
|
||||
* plugins: [search]
|
||||
* ```
|
||||
*
|
||||
* The basePath defaults to `/api`, meaning the default full internal
|
||||
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
||||
*/
|
||||
static fromConfig(config: Config, options?: { basePath?: string }) {
|
||||
const basePath = options?.basePath ?? '/api';
|
||||
const externalBaseUrl = config
|
||||
.getString('backend.baseUrl')
|
||||
.replace(/\/+$/, '');
|
||||
|
||||
const {
|
||||
listen: { host: listenHost = '::', port: listenPort },
|
||||
} = readHttpServerOptions(config.getConfig('backend'));
|
||||
const protocol = config.has('backend.https') ? 'https' : 'http';
|
||||
|
||||
// Translate bind-all to localhost, and support IPv6
|
||||
let host = listenHost;
|
||||
if (host === '::' || host === '') {
|
||||
// We use localhost instead of ::1, since IPv6-compatible systems should default
|
||||
// to using IPv6 when they see localhost, but if the system doesn't support IPv6
|
||||
// things will still work.
|
||||
host = 'localhost';
|
||||
} else if (host === '0.0.0.0') {
|
||||
host = '127.0.0.1';
|
||||
}
|
||||
if (host.includes(':')) {
|
||||
host = `[${host}]`;
|
||||
}
|
||||
|
||||
const internalBaseUrl = `${protocol}://${host}:${listenPort}`;
|
||||
|
||||
return new HostDiscovery(
|
||||
internalBaseUrl + basePath,
|
||||
externalBaseUrl + basePath,
|
||||
config.getOptionalConfig('discovery'),
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly internalBaseUrl: string,
|
||||
private readonly externalBaseUrl: string,
|
||||
private readonly discoveryConfig: Config | undefined,
|
||||
) {}
|
||||
|
||||
private getTargetFromConfig(pluginId: string, type: 'internal' | 'external') {
|
||||
const endpoints = this.discoveryConfig?.getOptionalConfigArray('endpoints');
|
||||
|
||||
const target = endpoints
|
||||
?.find(endpoint => endpoint.getStringArray('plugins').includes(pluginId))
|
||||
?.get<Target>('target');
|
||||
|
||||
if (!target) {
|
||||
const baseUrl =
|
||||
type === 'external' ? this.externalBaseUrl : this.internalBaseUrl;
|
||||
|
||||
return `${baseUrl}/${encodeURIComponent(pluginId)}`;
|
||||
}
|
||||
|
||||
if (typeof target === 'string') {
|
||||
return target.replace(
|
||||
/\{\{\s*pluginId\s*\}\}/g,
|
||||
encodeURIComponent(pluginId),
|
||||
);
|
||||
}
|
||||
|
||||
return target[type].replace(
|
||||
/\{\{\s*pluginId\s*\}\}/g,
|
||||
encodeURIComponent(pluginId),
|
||||
);
|
||||
}
|
||||
|
||||
async getBaseUrl(pluginId: string): Promise<string> {
|
||||
return this.getTargetFromConfig(pluginId, 'internal');
|
||||
}
|
||||
|
||||
async getExternalBaseUrl(pluginId: string): Promise<string> {
|
||||
return this.getTargetFromConfig(pluginId, 'external');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { HostDiscovery } from './HostDiscovery';
|
||||
|
||||
/** @public */
|
||||
export const discoveryServiceFactory = createServiceFactory({
|
||||
service: coreServices.discovery,
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
},
|
||||
async factory({ config }) {
|
||||
return HostDiscovery.fromConfig(config);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { discoveryServiceFactory } from './discoveryServiceFactory';
|
||||
export { HostDiscovery } from './HostDiscovery';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { lifecycleServiceFactory } from './lifecycleServiceFactory';
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
LifecycleService,
|
||||
LifecycleServiceShutdownHook,
|
||||
LifecycleServiceShutdownOptions,
|
||||
LifecycleServiceStartupHook,
|
||||
LifecycleServiceStartupOptions,
|
||||
LoggerService,
|
||||
PluginMetadataService,
|
||||
RootLifecycleService,
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @internal */
|
||||
export class BackendPluginLifecycleImpl implements LifecycleService {
|
||||
constructor(
|
||||
private readonly logger: LoggerService,
|
||||
private readonly rootLifecycle: RootLifecycleService,
|
||||
private readonly pluginMetadata: PluginMetadataService,
|
||||
) {}
|
||||
|
||||
#hasStarted = false;
|
||||
#startupTasks: Array<{
|
||||
hook: LifecycleServiceStartupHook;
|
||||
options?: LifecycleServiceStartupOptions;
|
||||
}> = [];
|
||||
|
||||
addStartupHook(
|
||||
hook: LifecycleServiceStartupHook,
|
||||
options?: LifecycleServiceStartupOptions,
|
||||
): void {
|
||||
if (this.#hasStarted) {
|
||||
throw new Error('Attempted to add startup hook after startup');
|
||||
}
|
||||
this.#startupTasks.push({ hook, options });
|
||||
}
|
||||
|
||||
async startup(): Promise<void> {
|
||||
if (this.#hasStarted) {
|
||||
return;
|
||||
}
|
||||
this.#hasStarted = true;
|
||||
|
||||
this.logger.debug(
|
||||
`Running ${this.#startupTasks.length} plugin startup tasks...`,
|
||||
);
|
||||
await Promise.all(
|
||||
this.#startupTasks.map(async ({ hook, options }) => {
|
||||
const logger = options?.logger ?? this.logger;
|
||||
try {
|
||||
await hook();
|
||||
logger.debug(`Plugin startup hook succeeded`);
|
||||
} catch (error) {
|
||||
logger.error(`Plugin startup hook failed, ${error}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
addShutdownHook(
|
||||
hook: LifecycleServiceShutdownHook,
|
||||
options?: LifecycleServiceShutdownOptions,
|
||||
): void {
|
||||
const plugin = this.pluginMetadata.getId();
|
||||
this.rootLifecycle.addShutdownHook(hook, {
|
||||
logger: options?.logger?.child({ plugin }) ?? this.logger,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const lifecycleServiceFactory = createServiceFactory({
|
||||
service: coreServices.lifecycle,
|
||||
deps: {
|
||||
logger: coreServices.logger,
|
||||
rootLifecycle: coreServices.rootLifecycle,
|
||||
pluginMetadata: coreServices.pluginMetadata,
|
||||
},
|
||||
async factory({ rootLifecycle, logger, pluginMetadata }) {
|
||||
return new BackendPluginLifecycleImpl(
|
||||
logger,
|
||||
rootLifecycle,
|
||||
pluginMetadata,
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { permissionsServiceFactory } from './permissionsServiceFactory';
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const permissionsServiceFactory = createServiceFactory({
|
||||
service: coreServices.permissions,
|
||||
deps: {
|
||||
auth: coreServices.auth,
|
||||
config: coreServices.rootConfig,
|
||||
discovery: coreServices.discovery,
|
||||
tokenManager: coreServices.tokenManager,
|
||||
},
|
||||
async factory({ auth, config, discovery, tokenManager }) {
|
||||
return ServerPermissionClient.fromConfig(config, {
|
||||
auth,
|
||||
discovery,
|
||||
tokenManager,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { rootConfigServiceFactory } from './rootConfigServiceFactory';
|
||||
export type { RootConfigFactoryOptions } from './rootConfigServiceFactory';
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
ConfigSources,
|
||||
RemoteConfigSourceOptions,
|
||||
} from '@backstage/config-loader';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface RootConfigFactoryOptions {
|
||||
/**
|
||||
* Process arguments to use instead of the default `process.argv()`.
|
||||
*/
|
||||
argv?: string[];
|
||||
|
||||
/**
|
||||
* Enables and sets options for remote configuration loading.
|
||||
*/
|
||||
remote?: Pick<RemoteConfigSourceOptions, 'reloadInterval'>;
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const rootConfigServiceFactory = createServiceFactory(
|
||||
(options?: RootConfigFactoryOptions) => ({
|
||||
service: coreServices.rootConfig,
|
||||
deps: {},
|
||||
async factory() {
|
||||
const source = ConfigSources.default({
|
||||
argv: options?.argv,
|
||||
remote: options?.remote,
|
||||
watch: options?.watch,
|
||||
});
|
||||
console.log(`Loading config from ${source}`);
|
||||
return await ConfigSources.toConfig(source);
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { rootLifecycleServiceFactory } from './rootLifecycleServiceFactory';
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { BackendLifecycleImpl } from './rootLifecycleServiceFactory';
|
||||
|
||||
describe('lifecycleService', () => {
|
||||
it('should execute registered shutdown hook', async () => {
|
||||
const service = new BackendLifecycleImpl(getVoidLogger());
|
||||
const hook = jest.fn();
|
||||
service.addShutdownHook(() => hook());
|
||||
// should not execute the hook more than once.
|
||||
await service.shutdown();
|
||||
await service.shutdown();
|
||||
await service.shutdown();
|
||||
expect(hook).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not throw errors', async () => {
|
||||
const service = new BackendLifecycleImpl(getVoidLogger());
|
||||
service.addShutdownHook(() => {
|
||||
throw new Error('oh no');
|
||||
});
|
||||
await expect(service.shutdown()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not throw async errors', async () => {
|
||||
const service = new BackendLifecycleImpl(getVoidLogger());
|
||||
service.addShutdownHook(async () => {
|
||||
throw new Error('oh no');
|
||||
});
|
||||
await expect(service.shutdown()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reject hooks after trigger', async () => {
|
||||
const service = new BackendLifecycleImpl(getVoidLogger());
|
||||
await service.startup();
|
||||
expect(() => {
|
||||
service.addStartupHook(() => {});
|
||||
}).toThrow('Attempted to add startup hook after startup');
|
||||
|
||||
await service.shutdown();
|
||||
expect(() => {
|
||||
service.addShutdownHook(() => {});
|
||||
}).toThrow('Attempted to add shutdown hook after shutdown');
|
||||
});
|
||||
});
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createServiceFactory,
|
||||
coreServices,
|
||||
LifecycleServiceStartupHook,
|
||||
LifecycleServiceStartupOptions,
|
||||
LifecycleServiceShutdownHook,
|
||||
LifecycleServiceShutdownOptions,
|
||||
RootLifecycleService,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @internal */
|
||||
export class BackendLifecycleImpl implements RootLifecycleService {
|
||||
constructor(private readonly logger: LoggerService) {}
|
||||
|
||||
#hasStarted = false;
|
||||
#startupTasks: Array<{
|
||||
hook: LifecycleServiceStartupHook;
|
||||
options?: LifecycleServiceStartupOptions;
|
||||
}> = [];
|
||||
|
||||
addStartupHook(
|
||||
hook: LifecycleServiceStartupHook,
|
||||
options?: LifecycleServiceStartupOptions,
|
||||
): void {
|
||||
if (this.#hasStarted) {
|
||||
throw new Error('Attempted to add startup hook after startup');
|
||||
}
|
||||
this.#startupTasks.push({ hook, options });
|
||||
}
|
||||
|
||||
async startup(): Promise<void> {
|
||||
if (this.#hasStarted) {
|
||||
return;
|
||||
}
|
||||
this.#hasStarted = true;
|
||||
|
||||
this.logger.debug(`Running ${this.#startupTasks.length} startup tasks...`);
|
||||
await Promise.all(
|
||||
this.#startupTasks.map(async ({ hook, options }) => {
|
||||
const logger = options?.logger ?? this.logger;
|
||||
try {
|
||||
await hook();
|
||||
logger.debug(`Startup hook succeeded`);
|
||||
} catch (error) {
|
||||
logger.error(`Startup hook failed, ${error}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#hasShutdown = false;
|
||||
#shutdownTasks: Array<{
|
||||
hook: LifecycleServiceShutdownHook;
|
||||
options?: LifecycleServiceShutdownOptions;
|
||||
}> = [];
|
||||
|
||||
addShutdownHook(
|
||||
hook: LifecycleServiceShutdownHook,
|
||||
options?: LifecycleServiceShutdownOptions,
|
||||
): void {
|
||||
if (this.#hasShutdown) {
|
||||
throw new Error('Attempted to add shutdown hook after shutdown');
|
||||
}
|
||||
this.#shutdownTasks.push({ hook, options });
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
if (this.#hasShutdown) {
|
||||
return;
|
||||
}
|
||||
this.#hasShutdown = true;
|
||||
|
||||
this.logger.debug(
|
||||
`Running ${this.#shutdownTasks.length} shutdown tasks...`,
|
||||
);
|
||||
await Promise.all(
|
||||
this.#shutdownTasks.map(async ({ hook, options }) => {
|
||||
const logger = options?.logger ?? this.logger;
|
||||
try {
|
||||
await hook();
|
||||
logger.debug(`Shutdown hook succeeded`);
|
||||
} catch (error) {
|
||||
logger.error(`Shutdown hook failed, ${error}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const rootLifecycleServiceFactory = createServiceFactory({
|
||||
service: coreServices.rootLifecycle,
|
||||
deps: {
|
||||
logger: coreServices.rootLogger,
|
||||
},
|
||||
async factory({ logger }) {
|
||||
return new BackendLifecycleImpl(logger);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { resolvePackagePath } from '@backstage/backend-common';
|
||||
import { Knex } from 'knex';
|
||||
import { DB_MIGRATIONS_TABLE } from './tables';
|
||||
|
||||
export async function migrateBackendTasks(knex: Knex): Promise<void> {
|
||||
const migrationsDir = resolvePackagePath(
|
||||
'@backstage/backend-defaults',
|
||||
'migrations/scheduler',
|
||||
);
|
||||
|
||||
await knex.migrate.latest({
|
||||
directory: migrationsDir,
|
||||
tableName: DB_MIGRATIONS_TABLE,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations';
|
||||
export const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks';
|
||||
|
||||
export type DbTasksRow = {
|
||||
id: string;
|
||||
settings_json: string;
|
||||
next_run_start_at: Date;
|
||||
current_run_ticket?: string;
|
||||
current_run_started_at?: Date | string;
|
||||
current_run_expires_at?: Date | string;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { schedulerServiceFactory } from './schedulerServiceFactory';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user