Merge branch 'master' of https://github.com/backstage/backstage into audit-log-bep
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-onelogin-provider': minor
|
||||
---
|
||||
|
||||
Separate out the OneLogin provider into its own module
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-graph': patch
|
||||
---
|
||||
|
||||
Add function to `EntityRelationsGraph` filter that excludes entities from graph
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Use `inherit` variant on OverflowTooltip underlying Typography component.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Exposed `DefaultSchedulerService`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Fix bug with missing Actions column after adding "pagination" prop to catalog table
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Internal minor refactors of the database connectors
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-react': patch
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Prevents Autocomplete dropdown from overlapping sidebar on hovering it
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli-common': patch
|
||||
---
|
||||
|
||||
The monorepo root check in `findPaths` will now accept a shorthand `workspaces` config in `package.json`, no longer requiring `workspaces.packages`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-user-settings-backend': patch
|
||||
'@backstage/plugin-user-settings-common': patch
|
||||
'@backstage/plugin-user-settings': patch
|
||||
---
|
||||
|
||||
Use signals to update user settings across sessions
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-tasks': patch
|
||||
---
|
||||
|
||||
More detailed deprecation messages
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
Changed the way to display entities in EntityPicker to use entityPresentationApi instead of humanizeEntityRef
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Remove Tech Radar menu item from sidebar of scaffolded app to align with removal of tech-radar plugin from backend
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Increase max wait time in debug:wait action to 10 minutes
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Updated to use the new `@backstage/plugin-auth-backend-module-onelogin-provider` implementation
|
||||
@@ -268,6 +268,7 @@ Olausson
|
||||
Oldsberg
|
||||
onboarding
|
||||
Onboarding
|
||||
onelogin
|
||||
OpenSearch
|
||||
OpenShift
|
||||
openssl
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
---
|
||||
title: Plugin Metadata
|
||||
status: provisional
|
||||
authors:
|
||||
- '@Rugvip'
|
||||
owners:
|
||||
- '@backstage/maintainers'
|
||||
project-areas:
|
||||
- core
|
||||
creation-date: yyyy-mm-dd
|
||||
---
|
||||
|
||||
<!--
|
||||
**Note:** When your BEP is complete, all these pre-existing comments should be removed
|
||||
|
||||
When editing BEPs, aim for tightly-scoped, single-topic PRs to keep discussions focused. If you disagree with what is already in a document, open a new PR with suggested changes.
|
||||
-->
|
||||
|
||||
# BEP: Plugin Metadata
|
||||
|
||||
<!-- Before merging the initial BEP PR, create a feature issue and update the below link. You can wait with this step until the BEP is ready to be merged. -->
|
||||
|
||||
[**Discussion Issue**](https://github.com/backstage/backstage/issues/25076)
|
||||
|
||||
- [Summary](#summary)
|
||||
- [Motivation](#motivation)
|
||||
- [Goals](#goals)
|
||||
- [Non-Goals](#non-goals)
|
||||
- [Proposal](#proposal)
|
||||
- [Design Details](#design-details)
|
||||
- [Release Plan](#release-plan)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Alternatives](#alternatives)
|
||||
|
||||
## Summary
|
||||
|
||||
The goal of this BEP is to define a strategy for how we define metadata for Backstage packages, as well as define a new set of metadata for how to associate packages with each other. The proposed solution is to store all plugin metadata in `package.json` under the `backstage` field, unless there is an existing field that can be used instead. Each piece of metadata is documented in the Backstage documentation, and as much of the metadata as possible is generated and validated by the Backstage CLI. The validation is structured in such a way that it does not interfere with internal Backstage setups, and only applies when publishing packages to NPM.
|
||||
|
||||
## Motivation
|
||||
|
||||
All Backstage adopters benefit from it being easy to find, install, configure, and validate the setup of plugins and modules. That's regardless of whether one is only using a minimal set of core plugins, or installing a large number of 3rd party features from the Backstage ecosystem. This proposal aims to build upon our [existing package metadata](https://backstage.io/docs/tooling/cli/build-system#package-roles) to introduce a new set of standardized fields that plugins can use to provide additional information about themselves.
|
||||
|
||||
This metadata will be used to improve tooling in and around Backstage, such as providing better validation and more powerful utilities in the Backstage CLI, and more information to be surfaced in association with plugins for better discoverability.
|
||||
|
||||
### Goals
|
||||
|
||||
We want to provide a new set of standardized fields that makes it possible to build tooling that improves the following interactions with the Backstage ecosystem:
|
||||
|
||||
- Browsing packages related to a plugin, i.e. listing all available library packages and frontend/backend variants of a specific plugin.
|
||||
- Discoverability of plugin modules. Given a plugin you should be able to identify all of its modules, and given a module you should be able to discovery its plugin.
|
||||
|
||||
Furthermore this proposal should also lay the foundation for how we define additional metadata fields in the future. How they are validated, documented, and surfaced in the Backstage ecosystem.
|
||||
|
||||
In relation to the above, this proposal will aim to define what existing `package.json` fields are known in the Backstage ecosystem, and what their purpose is. This will help avoid duplication of fields, and aim to reduce metadata fragmentation.
|
||||
|
||||
Where appropriate, this new metadata will be made required for all packages that are published using Backstage's tooling. This will ensure that packages across the ecosystem are consistent in how they provide metadata. This requirement should not be unnecessarily strict, ideally only requiring fields that either have sensible defaults or that can be inferred from workspace content.
|
||||
|
||||
These goals must all be reached in a way that does not risk intentional or unintentional disruption of the metadata. For example it should not be possible for an unrelated package to claim to be part of a group of plugin packages that it is not part of.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
The surfacing of extensive implementation metadata is not in scope for this proposal. This includes for example the extension point and extensions exported by a plugin, what route references a frontend plugin has, or what API endpoints a backend plugin exposes.
|
||||
|
||||
This proposal will not aim to standardize any other fields that are not included in the above [goals](#goals). It will provide the foundation for how to define such fields, but not the fields themselves. This includes any informational fields such as package lifecycle or usage, but also fields that provide functional requirements, such as plugin dependencies or environment restrictions.
|
||||
|
||||
This proposal does not aim to provide any direct solution for plugin support metadata as proposed in [#10256](https://github.com/backstage/backstage/pull/10256). Many of the fields defined as followups to this proposal may cover the support metadata use-case, but what is also important for such a solution is to be able to provide local overrides for the metadata, which this proposal does not cover. We do however want to make sure that this use-case is taken into consideration when defining new metadata fields.
|
||||
|
||||
## Proposal
|
||||
|
||||
The proposal is split into separate sections, each contributing to the overall solution in different ways.
|
||||
|
||||
### Validation & Automation
|
||||
|
||||
The purpose of this section is to define how we make it as simple as possible to populate the metadata fields, and how we ensure that the metadata is present and correct.
|
||||
|
||||
Validation and automation is split into two separate phases. The first is phase is manual tooling that is run by the developer and typically validated in CI, for example the `backstage-cli fix` command. The second phase is validation at the time of publishing the package, which is done by the `yarn prepack` command. By splitting the tooling into these two phases we can ensure that a minimal set of metadata is present for all packages published using the Backstage tooling, but at the same time avoid adding unnecessary friction to the development process.
|
||||
|
||||
An important aspect to take into consideration is that most Backstage packages are never published to NPM, as they're part of an internal monorepo and get built directly into an internal Backstage application. Defining the full set of metadata for these packages is likely to be wasteful, which means we should avoid building too many requirements for that path.
|
||||
|
||||
### Package Metadata and Documentation
|
||||
|
||||
This section defines our strategy for defining new metadata fields, and how they are documented.
|
||||
|
||||
To the furthest extent possible we will rely on established standards for `package.json` fields, but we will do so within the bounds of the intended usage of these fields. This means that we will only use existing fields for their intended purpose, and instead define new fields if no existing field is suitable. An index of existing fields is found in the [NPM documentation](https://docs.npmjs.com/cli/v10/configuring-npm/package-json), but we are not limited to this list. Any fields that we can find within the NPM ecosystem may be used, as long as they are well established, for example if there could be a use for the `prettier` field [defined by prettier](https://prettier.io/docs/en/configuration.html#sharing-configurations).
|
||||
|
||||
In the case of new fields we always define them to be part of the top-level `"backstage"` field. The assumption is that any field that we define will always be tied to the Backstage ecosystem, even in the case of general fields such as "lifecycle". The only exception to this is if we believe that a field is generic enough that it could genuinely become part of the broader NPM ecosystem.
|
||||
|
||||
All fields must be documented in a new section of the "Tooling" documentation on backstage.io. This documentation should include a description of the field, what it is used for, and what the expected values are. The documentation must also mention whether the field is expected to be filled in manually, or if it will be generated by tooling. Changes to this documentation must be approved by the Backstage core maintainers, it is not owned by individual project area maintainers.
|
||||
|
||||
### Package Relationships
|
||||
|
||||
This section defines how we associate packages from the same plugin with each other.
|
||||
|
||||
Packages that are part of the same plugin should always be managed within the same monorepo and workspace. This does not apply to modules for a plugin, which may be hosted separately, but may still need to refer to the plugin that it is a module for.
|
||||
|
||||
Each plugin package must define a `backstage.pluginId` field, which is the same identifier as is used in the implementation of the plugin. This field is inferred from the package name by the `backstage-cli repo fix` command if it is not present. It should only be defined for plugin package, for example `@backstage/errors` should not define a plugin ID. The `backstage.pluginId` field is required when publishing a package with a plugin or module role, or a library role with "plugin" in its name.
|
||||
|
||||
The package relationships are defined in the `backstage.pluginPackages` field. The value of the field is an object where each key is the role of the package as defined by the `backstage.role` field, and the value is the package name. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@backstage/plugin-catalog",
|
||||
"backstage": {
|
||||
"role": "frontend-plugin",
|
||||
"pluginId": "catalog",
|
||||
"pluginPackages": {
|
||||
"frontend-plugin": "@backstage/plugin-catalog",
|
||||
"backend-plugin": "@backstage/plugin-catalog-backend",
|
||||
"web-library": "@backstage/plugin-catalog-react",
|
||||
"node-library": "@backstage/plugin-catalog-node",
|
||||
"common-library": "@backstage/plugin-catalog-common"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `backstage.pluginPackages` field is generated and updated by the `backstage-cli repo fix` command based on the packages that are present in the workspace and their `backstage.pluginId` and `backstage.role` fields. There can only be a single package of each role with a given plugin ID. The `backstage.pluginPackages` field is required when publishing a package with a `backstage.pluginId` field that is not using a module role.
|
||||
|
||||
Module packages define their target plugin both via the `backstage.pluginId` field, as well as via `backstage.pluginPackage`. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@backstage/plugin-catalog-backend-module-github",
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module",
|
||||
"pluginId": "catalog",
|
||||
"pluginPackage": "@backstage/plugin-catalog-backend"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `backstage.pluginPackage` field is required when publishing a package with a module role.
|
||||
|
||||
## Design Details
|
||||
|
||||
### Package Relationships Conflict Resolution
|
||||
|
||||
Conflicts in the defined metadata are handled as follows:
|
||||
|
||||
- The union of all `pluginPackages` is always used. If there are multiple packages defined for the same role, conflicts are resolved using the `backstage.moved` field if possible, but otherwise all packages are rejected.
|
||||
- All plugin packages with references to each other must have the same `pluginId`, or they are all rejected.
|
||||
- A module must have the same `pluginId` as the plugin it is a module for, or it is rejected.
|
||||
- If the role of a package does not match the definition in the `pluginPackages` field, the package is rejected.
|
||||
|
||||
When metadata is "rejected" the consumer should either throw an error, or treat the conflicted fields as if they do not exist.
|
||||
|
||||
### References & Prior Art
|
||||
|
||||
The following resources have been used to inform this proposal:
|
||||
|
||||
- [package.json documentation](https://docs.npmjs.com/cli/v10/configuring-npm/package-json)
|
||||
- [VSCode Extensions](https://code.visualstudio.com/api/references/extension-manifest)
|
||||
|
||||
## Release Plan
|
||||
|
||||
The new metadata fields and any generation and validation of fields will initially be rolled out as part of the `repo fix` command in the Backstage CLI. In addition we will also add validation to the `yarn prepack` command in such a way that it does not interfere with backend package bundling, but does apply when packing packages for publishing. These changes will all be rolled out in a single release of the Backstage CLI, meaning that anyone publishing Backstage packages using the Backstage CLI will be required to provide the new metadata.
|
||||
|
||||
## Dependencies
|
||||
|
||||
None
|
||||
|
||||
## Alternatives
|
||||
|
||||
### Simplified Library Relationships
|
||||
|
||||
Rather than listing libraries for each role, we could simply have an array of libraries that are available for the plugin:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@backstage/plugin-catalog",
|
||||
"backstage": {
|
||||
"role": "frontend-plugin",
|
||||
"pluginId": "catalog",
|
||||
"pluginPackages": {
|
||||
"frontend-plugin": "@backstage/plugin-catalog",
|
||||
"backend-plugin": "@backstage/plugin-catalog-backend",
|
||||
"libraries": [
|
||||
"@backstage/plugin-catalog-react",
|
||||
"@backstage/plugin-catalog-node",
|
||||
"@backstage/plugin-catalog-common"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A benefit of this approach is that it keeps the metadata simpler and reduces the need for conflict resolution logic since we can use the union of all listed library packages.
|
||||
|
||||
It also allows for the use-case of having multiple libraries of the same role, in case that would provide a benefit. Examples of this are the `@backstage/catalog-client` and `@backstage/catalog-model` packages.
|
||||
|
||||
A downside of this approach could be that it may encourage a larger number of library packages, which is not necessarily what we want. Another downside is that the role of each package is not immediately available.
|
||||
|
||||
### Separate Metadata File
|
||||
|
||||
This proposal suggests that we keep metadata in the `package.json` file. An alternative would be to define a separate metadata file, for example `backstage.json` or `backstage.yaml`.
|
||||
|
||||
A benefit of this approach is that we do not pollute the `package.json` file with additional metadata, which can help keep overall metadata easier to browse and understand. It also allows us to use our own format, such as allowing comments in JSON, or using YAML for easier readability.
|
||||
|
||||
A downside of this approach is that it introduces a bit more complexity and friction in the tooling and package publishing, because we need to make sure that the metadata file is always included in the published package. We also end up with additional logic for finding and parsing this file, whereas resolving and parsing `package.json` is already a solved problem.
|
||||
|
||||
One of the larger benefits of keeping package metadata `package.json` is that it makes the data immediately available in registry APIs. It avoids the need to download and parse additional files to get the metadata for a package. For this reason we believe that we should stick to using `package.json` for metadata for the time being. If we for some reason in the future find that we need to use a separate file, for example because the metadata becomes too large, we can always evolve the `backstage` field in a couple of different ways:
|
||||
|
||||
```json
|
||||
{
|
||||
// Moving all data to a separate file
|
||||
"backstage": "backstage.json"
|
||||
}
|
||||
{
|
||||
// Moving some data to a separate file
|
||||
"backstage": {
|
||||
"role": "frontend-plugin",
|
||||
"metadata": "backstage.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -94,6 +94,9 @@ backend:
|
||||
options:
|
||||
token: ${CICD_TOKEN}
|
||||
subject: cicd-system-completion-events
|
||||
# Restrictions are optional; see below
|
||||
accessRestrictions:
|
||||
- plugin: events
|
||||
- type: static
|
||||
options:
|
||||
token: ${ADMIN_CURL_TOKEN}
|
||||
@@ -260,3 +263,97 @@ header:
|
||||
```yaml
|
||||
Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW
|
||||
```
|
||||
|
||||
## Access Restrictions
|
||||
|
||||
Each `externalAccess` entry may optionally have an `accessRestrictions` key,
|
||||
which limits what that particular access method can do. Let's look at an
|
||||
example:
|
||||
|
||||
```yaml title="in e.g. app-config.production.yaml"
|
||||
backend:
|
||||
auth:
|
||||
externalAccess:
|
||||
- type: static
|
||||
options:
|
||||
token: ${CICD_TOKEN}
|
||||
subject: cicd-system-completion-events
|
||||
accessRestrictions:
|
||||
- plugin: events
|
||||
```
|
||||
|
||||
In this short example there's only one entry. It says that for anyone trying to
|
||||
make access with the CICD token, they will be rejected if they try to contact
|
||||
anything but the `events` backend plugin. You could add additional entries to
|
||||
the array that allow targeting more plugins if that's what you want.
|
||||
|
||||
:::note Note
|
||||
|
||||
If no `accessRestrictions` are added, the access method has unlimited access to
|
||||
all functionality of all plugins. It is recommended that you try to specify
|
||||
access restrictions whenever possible, to reduce risk.
|
||||
|
||||
:::
|
||||
|
||||
Each entry has one or more of the following fields:
|
||||
|
||||
- **`plugin`**: Required. A plugin ID as a string, for example `'catalog'`. Permits
|
||||
access to make requests to this plugin. Can be further refined by setting
|
||||
additional fields as per below.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
accessRestrictions:
|
||||
# access to any other plugin will be rejected
|
||||
- plugin: my-plugin
|
||||
```
|
||||
|
||||
- **`permission`**: Optional. A collection (comma/space separated string or
|
||||
string array) of permission names. If given, this method is limited to only
|
||||
performing actions with these named permissions in the plugin with the ID
|
||||
given above.
|
||||
|
||||
Note that this only applies where permissions checks are enabled in the first
|
||||
place. Endpoints that are not protected by the permissions system at all, are
|
||||
not affected by this setting.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
accessRestrictions:
|
||||
- plugin: my-plugin
|
||||
# Any other permission check will be rejected.
|
||||
permission:
|
||||
- my-plugin.add-item
|
||||
- my-plugin.remove-item
|
||||
# Also supports the shorthand form:
|
||||
# permission: my-plugin.add-item, my-plugin.remove-item
|
||||
```
|
||||
|
||||
- **`permissionAttribute`**: Optional. A key-value object of permission attributes
|
||||
where each value is a collection (comma/space separated string or string
|
||||
array) of allowed such values. If given, this method is limited to only
|
||||
performing actions whose permissions have these attributes.
|
||||
|
||||
Note that this only applies where permissions checks are
|
||||
enabled in the first place. Endpoints that are not protected by
|
||||
the permissions system at all, are not affected by this
|
||||
setting.
|
||||
|
||||
In practice, this is typically used to limit by the `action` attribute, for
|
||||
`'create'`, `'read'`, `'update'`, or `'delete'` values.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
accessRestrictions:
|
||||
- plugin: my-plugin
|
||||
permissionAttribute:
|
||||
# Updates and deletes will be rejected.
|
||||
action:
|
||||
- create
|
||||
- read
|
||||
# Also supports the shorthand form:
|
||||
# action: create, read
|
||||
```
|
||||
|
||||
@@ -5,4 +5,136 @@ sidebar_label: Auth
|
||||
description: Documentation for the Auth service
|
||||
---
|
||||
|
||||
TODO
|
||||
This service deals with the generation and verification of tokens and their
|
||||
associated representations as credentials objects. You can use it for validating
|
||||
incoming tokens, and generating tokens for calling other services.
|
||||
|
||||
If you want to deal with credentials specifically in the HTTP request/response
|
||||
flow, see also [the `httpAuth` service](./http-auth.md). If you want to extract
|
||||
more details about authenticated users such as their ownership entity refs, use
|
||||
[the `userInfo` service](./user-info.md).
|
||||
|
||||
## Using the Service
|
||||
|
||||
In the following code examples, the `auth` and `httpAuth` variables are assumed
|
||||
to be dependency-injected instances of the `coreServices.auth` and
|
||||
`coreServices.httpAuth` service, respectively. For a backend plugin, it might
|
||||
look like this:
|
||||
|
||||
```ts
|
||||
export default createBackendPlugin({
|
||||
pluginId: 'my-plugin',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
auth: coreServices.auth,
|
||||
httpAuth: coreServices.httpAuth,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
},
|
||||
async init({ auth, httpAuth, httpRouter }) {
|
||||
// Your code goes here
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Creating Request Tokens
|
||||
|
||||
If you need to create a token that can be used for making a request to another
|
||||
backend plugin:
|
||||
|
||||
```ts
|
||||
const { token } = await auth.getPluginRequestToken({
|
||||
onBehalfOf: await auth.getOwnServiceCredentials(),
|
||||
targetPluginId: 'catalog',
|
||||
});
|
||||
```
|
||||
|
||||
:::note Note
|
||||
|
||||
Never store and reuse tokens. Always call `getPluginRequestToken` immediately
|
||||
before making a request. Otherwise you run the risk of running into permission
|
||||
problems when expired tokens are being used for requests.
|
||||
|
||||
:::
|
||||
|
||||
This example is suitable when you need to make the request "as your own plugin",
|
||||
i.e. when your code is the original initiator of the call. An example of this
|
||||
could be periodic batch processes that index content in another service.
|
||||
|
||||
In situations where you are making a call on-behalf-of someone else, for example
|
||||
when making upstream requests inside a request handler, please always instead
|
||||
use the extracted credentials from the request.
|
||||
|
||||
```ts
|
||||
router.get('/makes-calls', async (req, res) => {
|
||||
const { token } = await auth.getPluginRequestToken({
|
||||
onBehalfOf: await httpAuth.credentials(req),
|
||||
targetPluginId: 'catalog',
|
||||
});
|
||||
// make a call using the token
|
||||
```
|
||||
|
||||
This ensures that the original caller and their associated permissions are
|
||||
properly carried along with the request chain. See [the `httpAuth` service docs](./http-auth.md)
|
||||
for more details.
|
||||
|
||||
The [service to service auth docs](../../auth/service-to-service-auth.md)
|
||||
contain more details about how to properly use tokens in your HTTP request
|
||||
paths.
|
||||
|
||||
### Authorizing Tokens
|
||||
|
||||
Most plugins should not deal with incoming request tokens directly at all, but
|
||||
rather use [`httpAuth.credentials`](./http-auth.md) instead as part of their
|
||||
request handlers. But in the rare cases where you are holding an incoming token
|
||||
and want to validate it and turn it into a credentials object, you can do so:
|
||||
|
||||
```ts
|
||||
const credentials = await auth.authenticate(token);
|
||||
```
|
||||
|
||||
There is an optional second parameter that you can set to `{ allowLimitedAccess:
|
||||
true }` if you specifically built a plugin that deals with cookie based access,
|
||||
which is rare.
|
||||
|
||||
### Inspecting Credentials
|
||||
|
||||
The `auth` service also contains facilities for working with credentials
|
||||
objects. For example checking what type of principal (caller type - e.g. user or
|
||||
service) they represent. For example:
|
||||
|
||||
```ts
|
||||
if (auth.isPrincipal(credentials, 'user)) {
|
||||
// In here, the TypeScript type of the credentials object has been properly
|
||||
// narrowed to `BackstageCredentials<BackstageUserPrincipal>` so you can
|
||||
// access its specific properties such as `credentials.principal.userEntityRef`.
|
||||
}
|
||||
```
|
||||
|
||||
## Configuring the service
|
||||
|
||||
:::note Note
|
||||
|
||||
The `auth` service is not suitable for having its implementation replaced
|
||||
entirely in your private repo. If you desire additional service auth related
|
||||
features, don't hesitate to [file an issue](https://github.com/backstage/backstage/issues/new/choose)
|
||||
or [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) to the open source features.
|
||||
|
||||
:::
|
||||
|
||||
For configuring service-to-service access methods, see [the auth docs](../../auth/service-to-service-auth.md).
|
||||
|
||||
The default auth policy requires all requests to be authenticated with either
|
||||
user or service credentials. This can be disabled by setting the
|
||||
`backend.auth.dangerouslyDisableDefaultAuthPolicy` app-config flag to `true`.
|
||||
Disabling this check means that the backend will no longer block unauthenticated
|
||||
requests, but instead allow them to pass through to plugins. Do not do this in
|
||||
production unless absolutely necessary.
|
||||
|
||||
If permissions are enabled, unauthenticated requests will be treated exactly as
|
||||
such, leaving it to the permission policy to determine what permissions should
|
||||
be allowed for an unauthenticated identity. Note that this will also apply to
|
||||
service-to-service calls between plugins unless you configure credentials for
|
||||
service calls.
|
||||
|
||||
@@ -5,4 +5,102 @@ sidebar_label: Http Auth
|
||||
description: Documentation for the Http Auth service
|
||||
---
|
||||
|
||||
TODO
|
||||
The `httpAuth` service deals with submitting and receiving credentials on Express
|
||||
HTTP request/response objects. This service is frequently used in plugins that
|
||||
have REST interfaces.
|
||||
|
||||
If you want to deal with raw tokens and do low level credentials handling, see
|
||||
also [the `auth` service](./auth.md). If you want to extract more details about
|
||||
authenticated users such as their ownership entity refs, use [the `userInfo` service](./user-info.md).
|
||||
|
||||
## Using the Service
|
||||
|
||||
In the following code examples, the `auth` and `httpAuth` variables are assumed
|
||||
to be dependency-injected instances of the `coreServices.auth` and
|
||||
`coreServices.httpAuth` service, respectively. For a backend plugin, it might
|
||||
look like this:
|
||||
|
||||
```ts
|
||||
export default createBackendPlugin({
|
||||
pluginId: 'my-plugin',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
auth: coreServices.auth,
|
||||
httpAuth: coreServices.httpAuth,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
},
|
||||
async init({ auth, httpAuth, httpRouter }) {
|
||||
// Your code goes here
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Getting Request Credentials
|
||||
|
||||
If you need to extract the validated credentials out of an incoming request, you
|
||||
can do so like this:
|
||||
|
||||
```ts
|
||||
router.get('/some-request', async (req, res) => {
|
||||
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
|
||||
// Do something with the credentials here
|
||||
```
|
||||
|
||||
The second argument is optional, but in this example we specified that we only
|
||||
want to allow user based requests. The credentials returned will then have a
|
||||
narrowed TypeScript type that reflects that the principal is known to be of the
|
||||
user type. This second argument can also specify `allowLimitedAccess: true` if
|
||||
you specifically built a plugin that deals with cookie based access, which is
|
||||
rare.
|
||||
|
||||
The default is to accept both service and user credentials (excluding limited
|
||||
access), but in the example above, any attempt to call this endpoint with
|
||||
service credentials will result in an Unauthorized error being thrown.
|
||||
|
||||
:::note Note
|
||||
|
||||
You don't need to call `httpAuth.credentials` _just_ to ensure that incoming
|
||||
credentials are valid in the first place; only use this method if you actually
|
||||
need to act upon the credentials somehow. The Backstage backend framework will have
|
||||
ensured the actual validity of any incoming token before your backend code is
|
||||
reached. The policy for these upfront framework level rules is controlled using
|
||||
[the `httpRouter` service](./http-router.md) when you register your routes.
|
||||
|
||||
:::
|
||||
|
||||
If you want to further work with the credentials object, [the `auth` service](./auth.md)
|
||||
has helper methods for that.
|
||||
|
||||
### Issuing Cookies
|
||||
|
||||
For some rare use cases, plugins may want to issue cookies with _limited access_
|
||||
user credentials. This is mostly relevant when browsers need to be able to
|
||||
request static resources, such as in the TechDocs plugin.
|
||||
|
||||
Plugins should almost never interact with the cookie functionality of the
|
||||
`httpAuth` service directly. The framework has builtin handling of cookie
|
||||
creation/deletion requests on a dedicated well-known endpoint. All you normally
|
||||
have to do to accept limited user access is to inform [the `httpRouter` service](./http-router.md)
|
||||
when creating your route that you want to permit
|
||||
cookie based access for a specific route, and then setting `allowLimitedAccess`
|
||||
to `true` when extracting credentials.
|
||||
|
||||
Due to the above, we do not document the `httpAuth.issueUserCookie` method here.
|
||||
|
||||
## Configuring the service
|
||||
|
||||
:::note Note
|
||||
|
||||
The `httpAuth` service is not suitable for having its implementation replaced
|
||||
entirely in your private repo. If you desire additional service auth related
|
||||
features, don't hesitate to [file an issue](https://github.com/backstage/backstage/issues/new/choose)
|
||||
or [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) to the open source features.
|
||||
|
||||
:::
|
||||
|
||||
This service has no configuration options, but it abides by the policies you
|
||||
have set up using [the `httpRouter` service](./http-router.md) for your routes,
|
||||
if any.
|
||||
|
||||
@@ -5,7 +5,8 @@ sidebar_label: Http Router
|
||||
description: Documentation for the Http Router service
|
||||
---
|
||||
|
||||
One of the most common services is the HTTP router service which is used to expose HTTP endpoints for other plugins to consume.
|
||||
One of the most common services is the HTTP router service which is used to
|
||||
expose HTTP endpoints for other plugins to consume.
|
||||
|
||||
## Using the service
|
||||
|
||||
@@ -37,6 +38,36 @@ createBackendPlugin({
|
||||
});
|
||||
```
|
||||
|
||||
This service is also responsible for keeping track of the auth policies that
|
||||
apply to your routes. The default policy is to require that auth is present with
|
||||
every incoming request, and to accept both service and user credentials
|
||||
(excluding limited access tokens). You can override this while registering your
|
||||
routes. This dangerously allows unauthenticated access on a specific route:
|
||||
|
||||
```ts
|
||||
http.addAuthPolicy({
|
||||
path: '/static/:id',
|
||||
allow: 'unauthenticated',
|
||||
});
|
||||
```
|
||||
|
||||
Note that the path is exactly the same format as what you used in your routes,
|
||||
including placeholders.
|
||||
|
||||
If your plugin uses cookie based access (which is rare), you need to allow that
|
||||
as follows:
|
||||
|
||||
```ts
|
||||
http.addAuthPolicy({
|
||||
path: '/static/:id',
|
||||
allow: 'user-cookie',
|
||||
});
|
||||
```
|
||||
|
||||
For those routes you will also have to specify `allowLimitedAccess: true` when
|
||||
using the [`auth`](./auth.md) and [`httpAuth`](./http-auth.md) services to
|
||||
access the incoming credentials.
|
||||
|
||||
## Configuring the service
|
||||
|
||||
This service does not have any configuration options.
|
||||
|
||||
@@ -5,4 +5,63 @@ sidebar_label: User Info
|
||||
description: Documentation for the User Info service
|
||||
---
|
||||
|
||||
TODO
|
||||
This service lets you extract more information about a set of user credentials.
|
||||
Specifically, it can be used to extract the ownership entity refs for a user
|
||||
principal.
|
||||
|
||||
See also the [`auth`](./auth.md) and [`httpAuth`](./http-auth.md) services for
|
||||
general credentials handling.
|
||||
|
||||
## Using the Service
|
||||
|
||||
In the following code examples, the `userInfo`, `auth`, and `httpAuth` variables are assumed
|
||||
to be dependency-injected instances of the `coreServices.userInfo` and
|
||||
`coreServices.httpAuth` service, respectively. For a backend plugin, it might
|
||||
look like this:
|
||||
|
||||
```ts
|
||||
export default createBackendPlugin({
|
||||
pluginId: 'my-plugin',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
auth: coreServices.auth,
|
||||
httpAuth: coreServices.httpAuth,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
userInfo: coreServices.userInfo,
|
||||
},
|
||||
async init({ auth, httpAuth, httpRouter, userInfo }) {
|
||||
// Your code goes here
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Getting User Info
|
||||
|
||||
This example extracts some user credentials out of a request and fetches
|
||||
additional information about that principal.
|
||||
|
||||
```ts
|
||||
router.get('/some-request', async (req, res) => {
|
||||
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
|
||||
const info = await userInfo.getUserInfo(credentials);
|
||||
```
|
||||
|
||||
The `userInfo` service only deals with credentials that contain user principals,
|
||||
it won't accept requests for service principals. In our example code the initial
|
||||
credentials extraction limits it to user credentials upfront. If you have an
|
||||
endpoint that allows both user and service credentials, you may want to wrap
|
||||
your user info extraction in a principal type check:
|
||||
|
||||
```ts
|
||||
router.get('/some-request', async (req, res) => {
|
||||
const credentials = await httpAuth.credentials(req);
|
||||
if (auth.isPrincipal(credentials, 'user')) {
|
||||
const info = await userInfo.getUserInfo(credentials);
|
||||
// ...
|
||||
```
|
||||
|
||||
The user info contains data that was extracted during sign-in for the given
|
||||
user.
|
||||
|
||||
@@ -4,34 +4,68 @@ title: Linking in Local Packages
|
||||
description: How to link in other local packages into your Backstage monorepo
|
||||
---
|
||||
|
||||
It can often be useful to try out changes to the packages in the main Backstage
|
||||
repo within your own app. For example if you want to make modifications to
|
||||
`@backstage/core-plugin-api` and try them out in your app.
|
||||
## Why?
|
||||
|
||||
To link in external packages, add them to your `package.json` and `lerna.json`
|
||||
workspace paths. These can be either relative or absolute paths with or without
|
||||
globs. For example:
|
||||
If you are looking to make changes within the core Backstage repository and test
|
||||
those changes within your Backstage application, you will need to link the two
|
||||
together:
|
||||
|
||||
```json
|
||||
```text
|
||||
~/backstage // cloned from Github
|
||||
~/my-backstage-application // generated using npx
|
||||
```
|
||||
|
||||
For example, you might want to make modifications to `@backstage/core-plugin-api` and try them out in your company's
|
||||
instance of Backstage.
|
||||
|
||||
## Linking in Backstage NPM Packages
|
||||
|
||||
To link in external packages, add them to your root `package.json` and `lerna.json`
|
||||
`"workspace"` paths. These can be either relative or absolute paths with or without
|
||||
globs.
|
||||
|
||||
For example:
|
||||
|
||||
```json title="/lerna.json"
|
||||
...
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"plugins/*",
|
||||
"../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api
|
||||
],
|
||||
...
|
||||
```
|
||||
|
||||
Then reinstall packages to make yarn set up symlinks:
|
||||
```json title="/package.json"
|
||||
...
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"plugins/*",
|
||||
"../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api
|
||||
],
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
Now reinstall all packages from the root to make yarn set up symlinks from your application to the core Backstage clone:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Making Backstage Changes
|
||||
|
||||
With this in place you can now modify the `@backstage/core-plugin-api` package
|
||||
within the main repo, and have those changes be reflected and tested in your
|
||||
app. Simply run your app using `yarn dev` (or `yarn start` for just frontend) as
|
||||
normal.
|
||||
|
||||
Note that for backend packages you need to make sure that linked packages are
|
||||
## Common Problems
|
||||
|
||||
### Backend Issues
|
||||
|
||||
For backend packages you need to make sure that linked packages are
|
||||
not dependencies of any non-linked package. If you for example want to work on
|
||||
`@backstage/backend-common`, you need to also link in other backend plugins and
|
||||
packages that depend on `@backstage/backend-common`, or temporarily disable
|
||||
@@ -39,6 +73,8 @@ those plugins in your backend. This is because the transformation of backend
|
||||
module tree stops whenever a non-local package is encountered, and from that
|
||||
point node will `require` packages directly for that entire module subtree.
|
||||
|
||||
### Typescript Issues
|
||||
|
||||
Type checking can also have issues when linking in external packages, since the
|
||||
linked in packages will use the types in the external project and dependency
|
||||
version mismatches between the two projects may cause errors. To fix any of
|
||||
@@ -54,3 +90,17 @@ invalidate correctly for the linked in packages, causing type checking to not
|
||||
reflect changes made to types. You can work around this by either setting
|
||||
`compilerOptions.incremental = false` in `tsconfig.json`, or by deleting the
|
||||
types cache folder `dist-types` before running `yarn tsc`.
|
||||
|
||||
### Version Issues
|
||||
|
||||
While `yarn install` might not error, it does not mean that the linking worked properly.
|
||||
You will know that linking worked properly when:
|
||||
|
||||
1. Your Backstage application root `/node_modules/@backstage/[some package]` is a symlink
|
||||
2. Your Backstage application `/packages/app/node_modules` and `/packages/backend/node_modules` does
|
||||
not contain the package you are attempting to link!
|
||||
|
||||
If you see Yarn continuing to download the package you are trying to link from NPM, you might need to be
|
||||
explicit in your `package.json` version so that it exactly matches what you have in the cloned Backstage
|
||||
repository on your machine. For example, if you have cloned `/plugins/catalog` with version
|
||||
`"version": "1.19.1-next.1"` you will need to be explicit in your application to point to `"1.19.1-next.1"`.
|
||||
|
||||
@@ -22,7 +22,7 @@ We also assign clear definitions for each lifecycle:
|
||||
- **Use:** This technology is recommended for use by the majority of teams with a specific use case.
|
||||
- **Trial:** This technology has been evaluated for specific use cases and has showed clear benefits. Some teams adopt it in production, although it should be limited to low-impact projects as it might incur a higher risk.
|
||||
- **Assess:** This technology has the potential to be beneficial for the company. Some teams are evaluating it and using it in experimental projects. Using it in production comes with a high cost and risk due to lack of in-house knowledge, maintenance, and support.
|
||||
- **Hold:** We don’t want to further invest in this technology or we evaluated it and we don’t see it as beneficial for the company. Teams should not use it in new projects and should plan on migrating to a supported alternative if they use it for historical reasons. For broadly adopted technologies, the Radar should refer to a migration path to a supported alternative.
|
||||
- **Hold:** We don't want to further invest in this technology or we evaluated it and we don't see it as beneficial for the company. Teams should not use it in new projects and should plan on migrating to a supported alternative if they use it for historical reasons. For broadly adopted technologies, the Radar should refer to a migration path to a supported alternative.
|
||||
|
||||
Since rolling out the Tech Radar, it has become the source of truth when creating, maintaining, or evolving our software ecosystem. Spotify has dozens of entries in our Radar and it can scale quite well whilst being easy for our engineers and engineering managers to consume.
|
||||
|
||||
@@ -32,13 +32,13 @@ To learn about how you can bring the Tech Radar to your Backstage installation,
|
||||
|
||||
## A personal note
|
||||
|
||||
I want to thank both the Backstage team and Spotify. Firstly, I’ve been working with our internal version of Backstage for over a year, and the developer experience since open sourcing has been even more of a joy to work with. Secondly, the 10% hack time that Spotify generously provides to all engineers enabled me to open source the Tech Radar plugin.
|
||||
I want to thank both the Backstage team and Spotify. Firstly, I've been working with our internal version of Backstage for over a year, and the developer experience since open sourcing has been even more of a joy to work with. Secondly, the 10% hack time that Spotify generously provides to all engineers enabled me to open source the Tech Radar plugin.
|
||||
|
||||
Since open sourcing it, the community has shown great interest in yet another powerful use case of Backstage. There was also an enthusiastic open source contributor who volunteered to migrate the plugin to TypeScript and React Hooks [in just 29 minutes](https://github.com/backstage/backstage/issues/661) of opening the issue!
|
||||
|
||||
I can’t wait to see how others benefit from the Tech Radar in their organizations!
|
||||
I can't wait to see how others benefit from the Tech Radar in their organizations!
|
||||
|
||||
[lighthouse website audits]: https://backstage.io/blog/2020/04/06/lighthouse-plugin
|
||||
[tech radar plugin]: https://github.com/backstage/backstage/tree/master/plugins/tech-radar
|
||||
[tech radar plugin]: https://github.com/backstage/community-plugins/tree/main/workspaces/tech-radar/plugins/tech-radar
|
||||
[thoughtworks]: https://www.thoughtworks.com/radar
|
||||
[zalando]: https://opensource.zalando.com/tech-radar/
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: VeeCode Kong Service Manager
|
||||
author: VeeCode Platform
|
||||
authorUrl: https://platform.vee.codes/
|
||||
category: api-manager
|
||||
description: The Kong Service Manager plugin provides information about the kong service, a list of the routes it has and also offers the possibility of manipulating plugins without leaving the backstage.
|
||||
documentation: https://platform.vee.codes/plugin/Kong%20Service%20Manager/
|
||||
iconUrl: https://veecode-platform.github.io/support/imgs/logo_3.svg
|
||||
npmPackageName: '@veecode-platform/plugin-kong-service-manager'
|
||||
tags:
|
||||
- kong
|
||||
- api-manager
|
||||
- services
|
||||
- routes
|
||||
- plugins
|
||||
addedDate: '2024-05-27'
|
||||
@@ -175,6 +175,26 @@ const config: Config = {
|
||||
from: '/docs/features/software-templates/authorizing-parameters-steps-and-actions',
|
||||
to: '/docs/features/software-templates/authorizing-scaffolder-template-details',
|
||||
},
|
||||
{
|
||||
from: '/docs/local-dev/cli-commands/',
|
||||
to: '/docs/tooling/cli/commands/',
|
||||
},
|
||||
{
|
||||
from: '/docs/local-dev/cli-build-system/',
|
||||
to: '/docs/tooling/cli/build-system/',
|
||||
},
|
||||
{
|
||||
from: '/docs/local-dev/cli-overview/',
|
||||
to: '/docs/tooling/cli/overview/',
|
||||
},
|
||||
{
|
||||
from: '/docs/local-dev/linking-local-packages/',
|
||||
to: '/docs/tooling/local-dev/linking-local-packages',
|
||||
},
|
||||
{
|
||||
from: '/docs/local-dev/debugging/',
|
||||
to: '/docs/tooling/local-dev/debugging',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3,9 +3,20 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { DatabaseService } from '@backstage/backend-plugin-api';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { SchedulerService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public
|
||||
export class DefaultSchedulerService {
|
||||
// (undocumented)
|
||||
static create(options: {
|
||||
database: DatabaseService;
|
||||
logger: LoggerService;
|
||||
}): SchedulerService;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const schedulerServiceFactory: () => ServiceFactory<
|
||||
SchedulerService,
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './mysql';
|
||||
export * from './postgres';
|
||||
export * from './sqlite3';
|
||||
export { MysqlConnector } from './mysql';
|
||||
export { PgConnector } from './postgres';
|
||||
export { Sqlite3Connector } from './sqlite3';
|
||||
|
||||
@@ -25,7 +25,7 @@ import knexFactory, { Knex } from 'knex';
|
||||
import { merge, omit } from 'lodash';
|
||||
import limiterFactory from 'p-limit';
|
||||
import yn from 'yn';
|
||||
import { Connector, DatabaseConnector } from '../types';
|
||||
import { Connector } from '../types';
|
||||
import defaultNameOverride from './defaultNameOverride';
|
||||
import { mergeDatabaseConfig } from './mergeDatabaseConfig';
|
||||
|
||||
@@ -226,19 +226,6 @@ export async function dropMysqlDatabase(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL database connector.
|
||||
*
|
||||
* Exposes database connector functionality via an immutable object.
|
||||
*/
|
||||
export const mysqlConnector: DatabaseConnector = Object.freeze({
|
||||
createClient: createMysqlDatabaseClient,
|
||||
ensureDatabaseExists: ensureMysqlDatabaseExists,
|
||||
createNameOverride: defaultNameOverride,
|
||||
parseConnectionString: parseMysqlConnectionString,
|
||||
dropDatabase: dropMysqlDatabase,
|
||||
});
|
||||
|
||||
/**
|
||||
* Provides a config lookup path for a plugin's config block.
|
||||
*/
|
||||
@@ -248,31 +235,16 @@ function pluginPath(pluginId: string): string {
|
||||
|
||||
function normalizeConnection(
|
||||
connection: Knex.StaticConnectionConfig | JsonObject | string | undefined,
|
||||
client: string,
|
||||
): Partial<Knex.StaticConnectionConfig> {
|
||||
if (typeof connection === 'undefined' || connection === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return typeof connection === 'string' || connection instanceof String
|
||||
? mysqlConnector.parseConnectionString(connection as string, client)
|
||||
? parseMysqlConnectionString(connection as string)
|
||||
: connection;
|
||||
}
|
||||
|
||||
function createNameOverride(
|
||||
client: string,
|
||||
name: string,
|
||||
): Partial<Knex.Config> {
|
||||
try {
|
||||
return mysqlConnector.createNameOverride(name);
|
||||
} catch (e) {
|
||||
throw new InputError(
|
||||
`Unable to create database name override for '${client}' connector`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MysqlConnector implements Connector {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
@@ -281,7 +253,7 @@ export class MysqlConnector implements Connector {
|
||||
|
||||
async getClient(
|
||||
pluginId: string,
|
||||
deps?: {
|
||||
_deps?: {
|
||||
lifecycle: LifecycleService;
|
||||
pluginMetadata: PluginMetadataService;
|
||||
},
|
||||
@@ -293,7 +265,7 @@ export class MysqlConnector implements Connector {
|
||||
const databaseName = this.getDatabaseName(pluginId);
|
||||
if (databaseName && this.getEnsureExistsConfig(pluginId)) {
|
||||
try {
|
||||
await mysqlConnector.ensureDatabaseExists!(pluginConfig, databaseName);
|
||||
await ensureMysqlDatabaseExists(pluginConfig, databaseName);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`,
|
||||
@@ -313,10 +285,9 @@ export class MysqlConnector implements Connector {
|
||||
this.getDatabaseOverrides(pluginId),
|
||||
);
|
||||
|
||||
const client = mysqlConnector.createClient(
|
||||
const client = createMysqlDatabaseClient(
|
||||
pluginConfig,
|
||||
databaseClientOverrides,
|
||||
deps,
|
||||
);
|
||||
|
||||
return client;
|
||||
@@ -418,12 +389,9 @@ export class MysqlConnector implements Connector {
|
||||
* unless `pluginDivisionMode` is set to `schema`.
|
||||
*/
|
||||
private getConnectionConfig(pluginId: string): Knex.StaticConnectionConfig {
|
||||
const { client, overridden } = this.getClientType(pluginId);
|
||||
const { overridden } = this.getClientType(pluginId);
|
||||
|
||||
let baseConnection = normalizeConnection(
|
||||
this.config.get('connection'),
|
||||
this.config.getString('client'),
|
||||
);
|
||||
let baseConnection = normalizeConnection(this.config.get('connection'));
|
||||
|
||||
// Databases cannot be shared unless the `pluginDivisionMode` is set to `schema`. The
|
||||
// `database` property from the base connection is omitted unless `pluginDivisionMode`
|
||||
@@ -435,7 +403,6 @@ export class MysqlConnector implements Connector {
|
||||
// get and normalize optional plugin specific database connection
|
||||
const connection = normalizeConnection(
|
||||
this.config.getOptional(`${pluginPath(pluginId)}.connection`),
|
||||
client,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -473,8 +440,6 @@ export class MysqlConnector implements Connector {
|
||||
*/
|
||||
private getDatabaseOverrides(pluginId: string): Knex.Config {
|
||||
const databaseName = this.getDatabaseName(pluginId);
|
||||
return databaseName
|
||||
? createNameOverride(this.getClientType(pluginId).client, databaseName)
|
||||
: {};
|
||||
return databaseName ? defaultNameOverride(databaseName) : {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@ import {
|
||||
PluginMetadataService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { ForwardedError, InputError } from '@backstage/errors';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { merge, omit } from 'lodash';
|
||||
import limiterFactory from 'p-limit';
|
||||
import { Client } from 'pg';
|
||||
import { Connector, DatabaseConnector } from '../types';
|
||||
import { Connector } from '../types';
|
||||
import defaultNameOverride from './defaultNameOverride';
|
||||
import defaultSchemaOverride from './defaultSchemaOverride';
|
||||
import { mergeDatabaseConfig } from './mergeDatabaseConfig';
|
||||
@@ -232,16 +232,6 @@ export async function dropPgDatabase(
|
||||
}
|
||||
}
|
||||
|
||||
export const pgConnector: DatabaseConnector = Object.freeze({
|
||||
createClient: createPgDatabaseClient,
|
||||
ensureDatabaseExists: ensurePgDatabaseExists,
|
||||
ensureSchemaExists: ensurePgSchemaExists,
|
||||
createNameOverride: defaultNameOverride,
|
||||
createSchemaOverride: defaultSchemaOverride,
|
||||
parseConnectionString: parsePgConnectionString,
|
||||
dropDatabase: dropPgDatabase,
|
||||
});
|
||||
|
||||
/**
|
||||
* Provides a config lookup path for a plugin's config block.
|
||||
*/
|
||||
@@ -251,45 +241,16 @@ function pluginPath(pluginId: string): string {
|
||||
|
||||
function normalizeConnection(
|
||||
connection: Knex.StaticConnectionConfig | JsonObject | string | undefined,
|
||||
client: string,
|
||||
): Partial<Knex.StaticConnectionConfig> {
|
||||
if (typeof connection === 'undefined' || connection === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return typeof connection === 'string' || connection instanceof String
|
||||
? pgConnector.parseConnectionString(connection as string, client)
|
||||
? parsePgConnectionString(connection as string)
|
||||
: connection;
|
||||
}
|
||||
|
||||
function createSchemaOverride(
|
||||
client: string,
|
||||
name: string,
|
||||
): Partial<Knex.Config | undefined> {
|
||||
try {
|
||||
return pgConnector.createSchemaOverride?.(name);
|
||||
} catch (e) {
|
||||
throw new InputError(
|
||||
`Unable to create database schema override for '${client}' connector`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createNameOverride(
|
||||
client: string,
|
||||
name: string,
|
||||
): Partial<Knex.Config> {
|
||||
try {
|
||||
return pgConnector.createNameOverride(name);
|
||||
} catch (e) {
|
||||
throw new InputError(
|
||||
`Unable to create database name override for '${client}' connector`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class PgConnector implements Connector {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
@@ -298,7 +259,7 @@ export class PgConnector implements Connector {
|
||||
|
||||
async getClient(
|
||||
pluginId: string,
|
||||
deps?: {
|
||||
_deps?: {
|
||||
lifecycle: LifecycleService;
|
||||
pluginMetadata: PluginMetadataService;
|
||||
},
|
||||
@@ -310,7 +271,7 @@ export class PgConnector implements Connector {
|
||||
const databaseName = this.getDatabaseName(pluginId);
|
||||
if (databaseName && this.getEnsureExistsConfig(pluginId)) {
|
||||
try {
|
||||
await pgConnector.ensureDatabaseExists!(pluginConfig, databaseName);
|
||||
await ensurePgDatabaseExists(pluginConfig, databaseName);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`,
|
||||
@@ -320,13 +281,13 @@ export class PgConnector implements Connector {
|
||||
|
||||
let schemaOverrides;
|
||||
if (this.getPluginDivisionModeConfig() === 'schema') {
|
||||
schemaOverrides = this.getSchemaOverrides(pluginId);
|
||||
schemaOverrides = defaultSchemaOverride(pluginId);
|
||||
if (
|
||||
this.getEnsureSchemaExistsConfig(pluginId) ||
|
||||
this.getEnsureExistsConfig(pluginId)
|
||||
) {
|
||||
try {
|
||||
await pgConnector.ensureSchemaExists!(pluginConfig, pluginId);
|
||||
await ensurePgSchemaExists(pluginConfig, pluginId);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`,
|
||||
@@ -341,10 +302,9 @@ export class PgConnector implements Connector {
|
||||
schemaOverrides,
|
||||
);
|
||||
|
||||
const client = pgConnector.createClient(
|
||||
const client = createPgDatabaseClient(
|
||||
pluginConfig,
|
||||
databaseClientOverrides,
|
||||
deps,
|
||||
);
|
||||
|
||||
return client;
|
||||
@@ -465,12 +425,9 @@ export class PgConnector implements Connector {
|
||||
* unless `pluginDivisionMode` is set to `schema`.
|
||||
*/
|
||||
private getConnectionConfig(pluginId: string): Knex.StaticConnectionConfig {
|
||||
const { client, overridden } = this.getClientType(pluginId);
|
||||
const { overridden } = this.getClientType(pluginId);
|
||||
|
||||
let baseConnection = normalizeConnection(
|
||||
this.config.get('connection'),
|
||||
this.config.getString('client'),
|
||||
);
|
||||
let baseConnection = normalizeConnection(this.config.get('connection'));
|
||||
|
||||
// Databases cannot be shared unless the `pluginDivisionMode` is set to `schema`. The
|
||||
// `database` property from the base connection is omitted unless `pluginDivisionMode`
|
||||
@@ -482,7 +439,6 @@ export class PgConnector implements Connector {
|
||||
// get and normalize optional plugin specific database connection
|
||||
const connection = normalizeConnection(
|
||||
this.config.getOptional(`${pluginPath(pluginId)}.connection`),
|
||||
client,
|
||||
);
|
||||
|
||||
(
|
||||
@@ -516,17 +472,6 @@ export class PgConnector implements Connector {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a partial `Knex.Config` database schema override for a given
|
||||
* plugin.
|
||||
*
|
||||
* @param pluginId - Target plugin to get database schema override
|
||||
* @returns Partial `Knex.Config` with database schema override
|
||||
*/
|
||||
private getSchemaOverrides(pluginId: string): Knex.Config | undefined {
|
||||
return createSchemaOverride(this.getClientType(pluginId).client, pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a partial `Knex.Config`• database name override for a given plugin.
|
||||
*
|
||||
@@ -535,8 +480,6 @@ export class PgConnector implements Connector {
|
||||
*/
|
||||
private getDatabaseOverrides(pluginId: string): Knex.Config {
|
||||
const databaseName = this.getDatabaseName(pluginId);
|
||||
return databaseName
|
||||
? createNameOverride(this.getClientType(pluginId).client, databaseName)
|
||||
: {};
|
||||
return databaseName ? defaultNameOverride(databaseName) : {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,12 @@ import {
|
||||
PluginMetadataService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { ensureDirSync } from 'fs-extra';
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { merge, omit } from 'lodash';
|
||||
import path from 'path';
|
||||
import { Connector, DatabaseConnector } from '../types';
|
||||
import { Connector } from '../types';
|
||||
import { mergeDatabaseConfig } from './mergeDatabaseConfig';
|
||||
|
||||
/**
|
||||
@@ -157,17 +156,6 @@ export function parseSqliteConnectionString(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite3 database connector.
|
||||
*
|
||||
* Exposes database connector functionality via an immutable object.
|
||||
*/
|
||||
export const sqliteConnector: DatabaseConnector = Object.freeze({
|
||||
createClient: createSqliteDatabaseClient,
|
||||
createNameOverride: createSqliteNameOverride,
|
||||
parseConnectionString: parseSqliteConnectionString,
|
||||
});
|
||||
|
||||
/**
|
||||
* Provides a config lookup path for a plugin's config block.
|
||||
*/
|
||||
@@ -177,31 +165,16 @@ function pluginPath(pluginId: string): string {
|
||||
|
||||
function normalizeConnection(
|
||||
connection: Knex.StaticConnectionConfig | JsonObject | string | undefined,
|
||||
client: string,
|
||||
): Partial<Knex.StaticConnectionConfig> {
|
||||
if (typeof connection === 'undefined' || connection === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return typeof connection === 'string' || connection instanceof String
|
||||
? sqliteConnector.parseConnectionString(connection as string, client)
|
||||
? parseSqliteConnectionString(connection as string)
|
||||
: connection;
|
||||
}
|
||||
|
||||
function createNameOverride(
|
||||
client: string,
|
||||
name: string,
|
||||
): Partial<Knex.Config> {
|
||||
try {
|
||||
return sqliteConnector.createNameOverride(name);
|
||||
} catch (e) {
|
||||
throw new InputError(
|
||||
`Unable to create database name override for '${client}' connector`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Sqlite3Connector implements Connector {
|
||||
constructor(private readonly config: Config) {}
|
||||
|
||||
@@ -228,7 +201,7 @@ export class Sqlite3Connector implements Connector {
|
||||
this.getDatabaseOverrides(pluginId),
|
||||
);
|
||||
|
||||
const client = sqliteConnector.createClient(
|
||||
const client = createSqliteDatabaseClient(
|
||||
pluginConfig,
|
||||
databaseClientOverrides,
|
||||
deps,
|
||||
@@ -340,10 +313,7 @@ export class Sqlite3Connector implements Connector {
|
||||
private getConnectionConfig(pluginId: string): Knex.StaticConnectionConfig {
|
||||
const { client, overridden } = this.getClientType(pluginId);
|
||||
|
||||
let baseConnection = normalizeConnection(
|
||||
this.config.get('connection'),
|
||||
this.config.getString('client'),
|
||||
);
|
||||
let baseConnection = normalizeConnection(this.config.get('connection'));
|
||||
|
||||
if (
|
||||
client.includes('sqlite3') &&
|
||||
@@ -366,7 +336,6 @@ export class Sqlite3Connector implements Connector {
|
||||
// get and normalize optional plugin specific database connection
|
||||
const connection = normalizeConnection(
|
||||
this.config.getOptional(`${pluginPath(pluginId)}.connection`),
|
||||
client,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -404,8 +373,6 @@ export class Sqlite3Connector implements Connector {
|
||||
*/
|
||||
private getDatabaseOverrides(pluginId: string): Knex.Config {
|
||||
const databaseName = this.getDatabaseName(pluginId);
|
||||
return databaseName
|
||||
? createNameOverride(this.getClientType(pluginId).client, databaseName)
|
||||
: {};
|
||||
return databaseName ? createSqliteNameOverride(databaseName) : {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { DefaultSchedulerService } from './lib/DefaultSchedulerService';
|
||||
export { schedulerServiceFactory } from './schedulerServiceFactory';
|
||||
|
||||
+13
-12
@@ -14,37 +14,38 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DatabaseManager, getVoidLogger } from '@backstage/backend-common';
|
||||
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
TestDatabaseId,
|
||||
TestDatabases,
|
||||
mockServices,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { Duration } from 'luxon';
|
||||
import waitForExpect from 'wait-for-expect';
|
||||
import { TaskScheduler } from './TaskScheduler';
|
||||
import { DefaultSchedulerService } from './DefaultSchedulerService';
|
||||
import { createTestScopedSignal } from './__testUtils__/createTestScopedSignal';
|
||||
import { DatabaseService } from '@backstage/backend-plugin-api';
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
describe('TaskScheduler', () => {
|
||||
const logger = getVoidLogger();
|
||||
const logger = mockServices.logger.mock();
|
||||
const databases = TestDatabases.create();
|
||||
const testScopedSignal = createTestScopedSignal();
|
||||
|
||||
async function createDatabase(
|
||||
databaseId: TestDatabaseId,
|
||||
): Promise<DatabaseManager> {
|
||||
): Promise<DatabaseService> {
|
||||
const knex = await databases.init(databaseId);
|
||||
const databaseManager: Partial<DatabaseManager> = {
|
||||
forPlugin: () => ({
|
||||
getClient: async () => knex,
|
||||
}),
|
||||
return {
|
||||
getClient: async () => knex,
|
||||
};
|
||||
return databaseManager as DatabaseManager;
|
||||
}
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'can return a working v1 plugin impl, %p',
|
||||
async databaseId => {
|
||||
const database = await createDatabase(databaseId);
|
||||
const manager = new TaskScheduler(database, logger).forPlugin('test');
|
||||
const manager = DefaultSchedulerService.create({ database, logger });
|
||||
const fn = jest.fn();
|
||||
|
||||
await manager.scheduleTask({
|
||||
@@ -65,7 +66,7 @@ describe('TaskScheduler', () => {
|
||||
'can return a working v2 plugin impl, %p',
|
||||
async databaseId => {
|
||||
const database = await createDatabase(databaseId);
|
||||
const manager = new TaskScheduler(database, logger).forPlugin('test');
|
||||
const manager = DefaultSchedulerService.create({ database, logger });
|
||||
const fn = jest.fn();
|
||||
|
||||
await manager.scheduleTask({
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 {
|
||||
DatabaseService,
|
||||
LoggerService,
|
||||
SchedulerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { once } from 'lodash';
|
||||
import { Duration } from 'luxon';
|
||||
import { migrateBackendTasks } from '../database/migrateBackendTasks';
|
||||
import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl';
|
||||
import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor';
|
||||
|
||||
/**
|
||||
* Default implementation of the task scheduler service.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class DefaultSchedulerService {
|
||||
static create(options: {
|
||||
database: DatabaseService;
|
||||
logger: LoggerService;
|
||||
}): SchedulerService {
|
||||
const databaseFactory = once(async () => {
|
||||
const knex = await options.database.getClient();
|
||||
|
||||
if (!options.database.migrations?.skip) {
|
||||
await migrateBackendTasks(knex);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
const janitor = new PluginTaskSchedulerJanitor({
|
||||
knex,
|
||||
waitBetweenRuns: Duration.fromObject({ minutes: 1 }),
|
||||
logger: options.logger,
|
||||
});
|
||||
janitor.start();
|
||||
}
|
||||
|
||||
return knex;
|
||||
});
|
||||
|
||||
return new PluginTaskSchedulerImpl(databaseFactory, options.logger);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
DatabaseManager,
|
||||
getRootLogger,
|
||||
LegacyRootDatabaseService,
|
||||
PluginDatabaseManager,
|
||||
} from '@backstage/backend-common';
|
||||
import {
|
||||
LoggerService,
|
||||
RootConfigService,
|
||||
SchedulerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { once } from 'lodash';
|
||||
import { Duration } from 'luxon';
|
||||
import { migrateBackendTasks } from '../database/migrateBackendTasks';
|
||||
import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl';
|
||||
import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor';
|
||||
|
||||
/**
|
||||
* Deals with the scheduling of distributed tasks.
|
||||
*/
|
||||
export class TaskScheduler {
|
||||
static fromConfig(
|
||||
config: RootConfigService,
|
||||
options?: {
|
||||
databaseManager?: LegacyRootDatabaseService;
|
||||
logger?: LoggerService;
|
||||
},
|
||||
): TaskScheduler {
|
||||
const databaseManager =
|
||||
options?.databaseManager ?? DatabaseManager.fromConfig(config);
|
||||
const logger = (options?.logger || getRootLogger()).child({
|
||||
type: 'taskManager',
|
||||
});
|
||||
return new TaskScheduler(databaseManager, logger);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly databaseManager: LegacyRootDatabaseService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Instantiates a task manager instance for the given plugin.
|
||||
*
|
||||
* @param pluginId - The unique ID of the plugin, for example "catalog"
|
||||
* @returns A {@link SchedulerService} instance
|
||||
*/
|
||||
forPlugin(pluginId: string): SchedulerService {
|
||||
return TaskScheduler.forPlugin({
|
||||
pluginId,
|
||||
databaseManager: this.databaseManager.forPlugin(pluginId),
|
||||
logger: this.logger,
|
||||
});
|
||||
}
|
||||
|
||||
static forPlugin(opts: {
|
||||
pluginId: string;
|
||||
databaseManager: PluginDatabaseManager;
|
||||
logger: LoggerService;
|
||||
}): SchedulerService {
|
||||
const databaseFactory = once(async () => {
|
||||
const knex = await opts.databaseManager.getClient();
|
||||
|
||||
if (!opts.databaseManager.migrations?.skip) {
|
||||
await migrateBackendTasks(knex);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
const janitor = new PluginTaskSchedulerJanitor({
|
||||
knex,
|
||||
waitBetweenRuns: Duration.fromObject({ minutes: 1 }),
|
||||
logger: opts.logger,
|
||||
});
|
||||
janitor.start();
|
||||
}
|
||||
|
||||
return knex;
|
||||
});
|
||||
|
||||
return new PluginTaskSchedulerImpl(databaseFactory, opts.logger);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { TaskScheduler } from './lib/TaskScheduler';
|
||||
import { DefaultSchedulerService } from './lib/DefaultSchedulerService';
|
||||
|
||||
/**
|
||||
* The default service factory for {@link @backstage/backend-plugin-api#coreServices.scheduler}.
|
||||
@@ -28,15 +28,10 @@ import { TaskScheduler } from './lib/TaskScheduler';
|
||||
export const schedulerServiceFactory = createServiceFactory({
|
||||
service: coreServices.scheduler,
|
||||
deps: {
|
||||
plugin: coreServices.pluginMetadata,
|
||||
database: coreServices.database,
|
||||
logger: coreServices.logger,
|
||||
},
|
||||
async factory({ plugin, database, logger }) {
|
||||
return TaskScheduler.forPlugin({
|
||||
pluginId: plugin.getId(),
|
||||
databaseManager: database,
|
||||
logger,
|
||||
});
|
||||
async factory({ database, logger }) {
|
||||
return DefaultSchedulerService.create({ database, logger });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -87,8 +87,9 @@ export class TaskScheduler {
|
||||
databaseManager: LegacyRootDatabaseService,
|
||||
logger: LoggerService,
|
||||
);
|
||||
// @deprecated
|
||||
forPlugin(pluginId: string): PluginTaskScheduler;
|
||||
// (undocumented)
|
||||
// @deprecated (undocumented)
|
||||
static forPlugin(opts: {
|
||||
pluginId: string;
|
||||
databaseManager: PluginDatabaseManager;
|
||||
|
||||
@@ -33,7 +33,7 @@ import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
* Deals with the scheduling of distributed tasks.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Please migrate to the new backend system, and depend on `coreServices.scheduler` from `@backstage/backend-plugin-api` instead.
|
||||
* @deprecated Please migrate to the new backend system, and depend on `coreServices.scheduler` from `@backstage/backend-plugin-api` instead, or use `DefaultSchedulerService` from `@backstage/backend-defaults`
|
||||
*/
|
||||
export class TaskScheduler {
|
||||
/**
|
||||
@@ -65,6 +65,7 @@ export class TaskScheduler {
|
||||
*
|
||||
* @param pluginId - The unique ID of the plugin, for example "catalog"
|
||||
* @returns A {@link PluginTaskScheduler} instance
|
||||
* @deprecated Please migrate to the new backend system, and depend on `coreServices.scheduler` from `@backstage/backend-plugin-api` instead, or use `DefaultSchedulerService` from `@backstage/backend-defaults`
|
||||
*/
|
||||
forPlugin(pluginId: string): PluginTaskScheduler {
|
||||
return TaskScheduler.forPlugin({
|
||||
@@ -74,6 +75,9 @@ export class TaskScheduler {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please migrate to the new backend system, and depend on `coreServices.scheduler` from `@backstage/backend-plugin-api` instead, or use `DefaultSchedulerService` from `@backstage/backend-defaults`
|
||||
*/
|
||||
static forPlugin(opts: {
|
||||
pluginId: string;
|
||||
databaseManager: PluginDatabaseManager;
|
||||
|
||||
@@ -19,6 +19,10 @@ import { resolve as resolvePath } from 'path';
|
||||
import { findPaths, findRootPath, findOwnDir, findOwnRootDir } from './paths';
|
||||
|
||||
describe('paths', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('findOwnDir and findOwnRootDir should find owns paths', () => {
|
||||
const dir = findOwnDir(__dirname);
|
||||
const root = findOwnRootDir(dir);
|
||||
@@ -92,4 +96,28 @@ describe('paths', () => {
|
||||
resolvePath(root, 'derp.txt'),
|
||||
);
|
||||
});
|
||||
|
||||
it('findPaths should find workspace root with object', () => {
|
||||
jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: { packages: [] } });
|
||||
jest.spyOn(process, 'cwd').mockReturnValue(__dirname);
|
||||
|
||||
const paths = findPaths(__dirname);
|
||||
|
||||
expect(paths.targetDir).toBe(
|
||||
resolvePath(__dirname, '../../cli-common/src'),
|
||||
);
|
||||
expect(paths.targetRoot).toBe(resolvePath(__dirname, '../../cli-common'));
|
||||
});
|
||||
|
||||
it('findPaths should find workspace root with array', () => {
|
||||
jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: [] });
|
||||
jest.spyOn(process, 'cwd').mockReturnValue(__dirname);
|
||||
|
||||
const paths = findPaths(__dirname);
|
||||
|
||||
expect(paths.targetDir).toBe(
|
||||
resolvePath(__dirname, '../../cli-common/src'),
|
||||
);
|
||||
expect(paths.targetRoot).toBe(resolvePath(__dirname, '../../cli-common'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -141,7 +141,7 @@ export function findPaths(searchDir: string): Paths {
|
||||
try {
|
||||
const content = fs.readFileSync(path, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
return Boolean(data.workspaces?.packages);
|
||||
return Boolean(data.workspaces);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse package.json file while searching for root, ${error}`,
|
||||
|
||||
@@ -53,7 +53,9 @@ export function OverflowTooltip(props: Props) {
|
||||
title={props.title ?? (props.text || '')}
|
||||
placement={props.placement}
|
||||
>
|
||||
<Typography className={classes.typo}>{props.text}</Typography>
|
||||
<Typography className={classes.typo} variant="inherit">
|
||||
{props.text}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { PropsWithChildren } from 'react';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import MapIcon from '@material-ui/icons/MyLocation';
|
||||
import LibraryBooks from '@material-ui/icons/LibraryBooks';
|
||||
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import LogoFull from './LogoFull';
|
||||
@@ -72,7 +71,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
{/* End global nav */}
|
||||
<SidebarDivider />
|
||||
<SidebarScrollWrapper>
|
||||
<SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
|
||||
{/* Items in this group will be scrollable if they run out of space */}
|
||||
</SidebarScrollWrapper>
|
||||
</SidebarGroup>
|
||||
<SidebarSpace />
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,2 @@
|
||||
# @yarnpkg/builder build output
|
||||
bundles/
|
||||
@@ -0,0 +1,13 @@
|
||||
# yarn-plugin-backstage
|
||||
|
||||
This yarn plugin adds a `backstage:` version protocol to yarn, which replaces
|
||||
specific version ranges for `@backstage/` packages. The only version range
|
||||
supported by this plugin is `backstage:^`, which has similar semantics to the
|
||||
corresponding [`workspace` range](https://yarnpkg.com/features/workspaces#cross-references) when using
|
||||
workspace dependencies; locally, the package will always resolve to the exact
|
||||
version specified in the manifest for the Backstage release listed in
|
||||
backstage.json. If the dependent package is published, this version will be
|
||||
prefixed by `^`.
|
||||
|
||||
**This plugin is still under active development, and requires some further
|
||||
testing before we recommend it for general use.**
|
||||
@@ -0,0 +1,11 @@
|
||||
## API Report File for "yarn-plugin-backstage"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { Plugin as Plugin_2 } from '@yarnpkg/core';
|
||||
|
||||
// @public (undocumented)
|
||||
const plugin: Plugin_2;
|
||||
export default plugin;
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: yarn-plugin-backstage
|
||||
title: yarn-plugin-backstage
|
||||
description: Yarn plugin for working with Backstage monorepos
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-node-library
|
||||
owner: maintainers
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "yarn-plugin-backstage",
|
||||
"version": "0.0.0",
|
||||
"description": "Yarn plugin for working with Backstage monorepos",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
},
|
||||
"private": true,
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/yarn-plugin"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "builder build plugin",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"start": "nodemon --",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"exec": "builder build plugin",
|
||||
"ext": "ts",
|
||||
"watch": "./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "workspace:^",
|
||||
"@backstage/release-manifests": "workspace:^",
|
||||
"@yarnpkg/core": "^4.0.3",
|
||||
"@yarnpkg/fslib": "^3.0.2",
|
||||
"semver": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@yarnpkg/builder": "^4.0.0",
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
@@ -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 const PROTOCOL = 'backstage:';
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Manifest, Workspace } from '@yarnpkg/core';
|
||||
import { npath, ppath } from '@yarnpkg/fslib';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
import { beforeWorkspacePacking } from './beforeWorkspacePacking';
|
||||
|
||||
jest.mock('@backstage/release-manifests', () => ({
|
||||
getManifestByVersion: jest.fn().mockResolvedValue({
|
||||
releaseVersion: '1.23.45',
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '3.2.1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
const makeWorkspace = (manifest: object) => {
|
||||
return {
|
||||
manifest: Manifest.fromText(JSON.stringify(manifest)),
|
||||
} as Workspace;
|
||||
};
|
||||
|
||||
describe('beforeWorkspacePacking', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(ppath, 'cwd')
|
||||
.mockReturnValue(npath.toPortablePath(mockDir.path));
|
||||
|
||||
jest
|
||||
.spyOn(process, 'cwd')
|
||||
.mockReturnValue(npath.toPortablePath(mockDir.path));
|
||||
|
||||
mockDir.setContent({
|
||||
'backstage.json': JSON.stringify({
|
||||
version: '1.23.45',
|
||||
}),
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe.each`
|
||||
dependencyType
|
||||
${'dependencies'}
|
||||
${'devDependencies'}
|
||||
${'optionalDependencies'}
|
||||
`('$dependencyType', ({ dependencyType }) => {
|
||||
it(`ignores ${dependencyType} that don't use the backstage: protocol`, () => {
|
||||
const result = {
|
||||
name: 'test-package',
|
||||
[dependencyType]: {
|
||||
foo: '^1.1.1',
|
||||
},
|
||||
};
|
||||
|
||||
beforeWorkspacePacking(makeWorkspace(result), result);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'test-package',
|
||||
[dependencyType]: {
|
||||
foo: '^1.1.1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`throws an error for any backstage: versions with a selector other than ^`, async () => {
|
||||
const result = {
|
||||
name: 'test-package',
|
||||
[dependencyType]: {
|
||||
'@backstage/core': 'backstage:^1.1.1',
|
||||
},
|
||||
};
|
||||
|
||||
await expect(() =>
|
||||
beforeWorkspacePacking(makeWorkspace(result), result),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('converts backstage:^ versions to the corresponding package version prefixed by ^', async () => {
|
||||
const result = {
|
||||
name: 'test-package',
|
||||
[dependencyType]: {
|
||||
'@backstage/core': 'backstage:^',
|
||||
},
|
||||
};
|
||||
|
||||
await beforeWorkspacePacking(makeWorkspace(result), result);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'test-package',
|
||||
[dependencyType]: {
|
||||
'@backstage/core': '^3.2.1',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Descriptor, Workspace, structUtils } from '@yarnpkg/core';
|
||||
import { getCurrentBackstageVersion, getPackageVersion } from '../util';
|
||||
import { PROTOCOL } from '../constants';
|
||||
|
||||
const getFinalDependencyType = (
|
||||
dependencyType: string,
|
||||
descriptor: Descriptor,
|
||||
workspace: Workspace,
|
||||
) => {
|
||||
if (dependencyType !== 'dependencies') {
|
||||
return dependencyType;
|
||||
}
|
||||
|
||||
return workspace.manifest.ensureDependencyMeta(
|
||||
structUtils.makeDescriptor(descriptor, 'unknown'),
|
||||
).optional
|
||||
? 'optionalDependencies'
|
||||
: dependencyType;
|
||||
};
|
||||
|
||||
export const beforeWorkspacePacking = async (
|
||||
workspace: Workspace,
|
||||
rawManifest: any,
|
||||
) => {
|
||||
const backstageVersion = getCurrentBackstageVersion();
|
||||
|
||||
for (const dependencyType of ['dependencies', 'devDependencies'] as const) {
|
||||
const entries = Array.from(
|
||||
workspace.manifest.getForScope(dependencyType).values(),
|
||||
).filter(descriptor => descriptor.range.startsWith(PROTOCOL));
|
||||
|
||||
for (const descriptor of entries) {
|
||||
const ident = structUtils.stringifyIdent(descriptor);
|
||||
const range = structUtils.parseRange(descriptor.range);
|
||||
|
||||
if (range.selector !== '^') {
|
||||
throw new Error(
|
||||
`Unexpected version range "${descriptor.range}" for dependency on "${ident}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const finalDependencyType = getFinalDependencyType(
|
||||
dependencyType,
|
||||
descriptor,
|
||||
workspace,
|
||||
);
|
||||
|
||||
rawManifest[finalDependencyType][ident] = `^${await getPackageVersion(
|
||||
structUtils.makeDescriptor(
|
||||
descriptor,
|
||||
`${PROTOCOL}${backstageVersion}`,
|
||||
),
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Yarn plugin for resolving package versions based on
|
||||
* a Backstage version manifest.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { Plugin } from '@yarnpkg/core';
|
||||
import { beforeWorkspacePacking } from './handlers/beforeWorkspacePacking';
|
||||
import { BackstageResolver } from './resolver/BackstageResolver';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
const plugin: Plugin = {
|
||||
hooks: {
|
||||
beforeWorkspacePacking,
|
||||
},
|
||||
resolvers: [BackstageResolver],
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { structUtils } from '@yarnpkg/core';
|
||||
import { npath, ppath } from '@yarnpkg/fslib';
|
||||
import { BackstageResolver } from './BackstageResolver';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
jest.mock('@backstage/release-manifests', () => ({
|
||||
getManifestByVersion: jest.fn().mockResolvedValue({
|
||||
releaseVersion: '1.23.45',
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '6.7.8',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('BackstageResolver', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
let backstageResolver: BackstageResolver;
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(ppath, 'cwd')
|
||||
.mockReturnValue(npath.toPortablePath(mockDir.path));
|
||||
|
||||
jest
|
||||
.spyOn(process, 'cwd')
|
||||
.mockReturnValue(npath.toPortablePath(mockDir.path));
|
||||
|
||||
mockDir.setContent({
|
||||
'backstage.json': JSON.stringify({
|
||||
version: '1.23.45',
|
||||
}),
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
},
|
||||
}),
|
||||
packages: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': 'backstage:^',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
backstageResolver = new BackstageResolver();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('supportsDescriptor', () => {
|
||||
it.each([
|
||||
['backstage:^'],
|
||||
['backstage:1.26.0-next.3'],
|
||||
['backstage:anything'],
|
||||
])('returns true for range "%s"', range => {
|
||||
expect(
|
||||
backstageResolver.supportsDescriptor(
|
||||
structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
range,
|
||||
),
|
||||
),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bindDescriptor', () => {
|
||||
describe('with range "backstage:^"', () => {
|
||||
it('returns a descriptor basedwith a version range for the current Backstage version', () => {
|
||||
expect(
|
||||
backstageResolver.bindDescriptor(
|
||||
structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:^',
|
||||
),
|
||||
),
|
||||
).toEqual(
|
||||
structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:1.23.45',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with range "backstage:1.23.45"', () => {
|
||||
it('throws an error', () => {
|
||||
expect(() =>
|
||||
backstageResolver.bindDescriptor(
|
||||
structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:1.23.45',
|
||||
),
|
||||
),
|
||||
).toThrow(/unsupported version range/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCandidates', () => {
|
||||
it('returns an npm: descriptor based on the manifest for the appropriate backstage version', async () => {
|
||||
const descriptor = structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:1.23.45',
|
||||
);
|
||||
|
||||
await expect(
|
||||
backstageResolver.getCandidates(descriptor),
|
||||
).resolves.toEqual([structUtils.makeLocator(descriptor, 'npm:6.7.8')]);
|
||||
});
|
||||
|
||||
it('rejects descriptors not using the backstage: protocol', async () => {
|
||||
await expect(
|
||||
backstageResolver.getCandidates(
|
||||
structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'npm:1.2.3',
|
||||
),
|
||||
),
|
||||
).rejects.toThrow(/unsupported version protocol/i);
|
||||
});
|
||||
|
||||
it('rejects backstage: ranges with a ^ shorthand version', async () => {
|
||||
await expect(
|
||||
backstageResolver.getCandidates(
|
||||
structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:^',
|
||||
),
|
||||
),
|
||||
).rejects.toThrow(/invalid backstage version/i);
|
||||
});
|
||||
|
||||
it('rejects backstage: ranges with a * shorthand version', async () => {
|
||||
await expect(
|
||||
backstageResolver.getCandidates(
|
||||
structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:*',
|
||||
),
|
||||
),
|
||||
).rejects.toThrow(/invalid backstage version/i);
|
||||
});
|
||||
|
||||
it('rejects backstage: ranges with an invalid version specified', async () => {
|
||||
await expect(
|
||||
backstageResolver.getCandidates(
|
||||
structUtils.makeDescriptor(
|
||||
structUtils.makeIdent('backstage', 'core'),
|
||||
'backstage:latest',
|
||||
),
|
||||
),
|
||||
).rejects.toThrow(/invalid backstage version/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import {
|
||||
structUtils,
|
||||
Descriptor,
|
||||
Locator,
|
||||
Package,
|
||||
Resolver,
|
||||
} from '@yarnpkg/core';
|
||||
import semver from 'semver';
|
||||
import { PROTOCOL } from '../constants';
|
||||
import { getCurrentBackstageVersion, getPackageVersion } from '../util';
|
||||
|
||||
export class BackstageResolver implements Resolver {
|
||||
static protocol = PROTOCOL;
|
||||
|
||||
supportsDescriptor = (descriptor: Descriptor) =>
|
||||
descriptor.range.startsWith(BackstageResolver.protocol);
|
||||
|
||||
shouldPersistResolution = () => true;
|
||||
|
||||
/**
|
||||
* Called for each dependency present in the dependency list of a package
|
||||
* definition. If it returns a new descriptor, this new descriptor will be
|
||||
* used.
|
||||
*
|
||||
* In this plugin, we convert the specific range "backstage:^" to
|
||||
* "backstage:<version from backstage.json>". This new range will be the one
|
||||
* stored in lockfile entries, ensuring that we re-resolve the package when
|
||||
* the version in backstage.json changes.
|
||||
*/
|
||||
bindDescriptor(descriptor: Descriptor): Descriptor {
|
||||
if (descriptor.range !== 'backstage:^') {
|
||||
throw new Error(
|
||||
`Unsupported version range "${
|
||||
descriptor.range
|
||||
}" for package ${structUtils.stringifyIdent(
|
||||
descriptor,
|
||||
)}. The backstage protocol only supports the range "backstage:^".`,
|
||||
);
|
||||
}
|
||||
|
||||
return structUtils.makeDescriptor(
|
||||
descriptor,
|
||||
`${PROTOCOL}${getCurrentBackstageVersion()}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a descriptor, return the list of locators that potentially satisfy
|
||||
* it. The implementation in this plugin converts a `backstage:` range with a
|
||||
* concrete version into the appropriate concrete npm version for that
|
||||
* backstage release.
|
||||
*/
|
||||
async getCandidates(descriptor: Descriptor): Promise<Locator[]> {
|
||||
const range = structUtils.parseRange(descriptor.range);
|
||||
if (range.protocol !== BackstageResolver.protocol) {
|
||||
throw new Error(
|
||||
`Unsupported version protocol in version range "${
|
||||
descriptor.range
|
||||
}" for package ${structUtils.stringifyIdent(descriptor)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!semver.valid(range.selector)) {
|
||||
throw new Error(
|
||||
`Invalid Backstage version string when resolving version for ${structUtils.stringifyIdent(
|
||||
descriptor,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
structUtils.makeLocator(
|
||||
descriptor,
|
||||
`npm:${await getPackageVersion(descriptor)}`,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This plugin does not need to support any locators itself, since the
|
||||
* `getCandidates` method will always convert `backstage:` versions into
|
||||
* `npm:` versions which can be handled as usual.
|
||||
*/
|
||||
supportsLocator = () => false;
|
||||
|
||||
/**
|
||||
* This resolver never transforms the packages that are actually depended on,
|
||||
* only replaces versions. As such there's never a need to add additional
|
||||
* dependencies.
|
||||
*/
|
||||
getResolutionDependencies = () => ({});
|
||||
|
||||
/**
|
||||
* Candidate versions produced by this resolver always use the `npm:`
|
||||
* protocol, so this function will never be called.
|
||||
*/
|
||||
async getSatisfying(): Promise<{ locators: Locator[]; sorted: boolean }> {
|
||||
throw new Error('Unreachable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Once transformed into locators (through getCandidates), the versions are
|
||||
* resolved by the NpmSemverResolver
|
||||
*/
|
||||
async resolve(): Promise<Package> {
|
||||
throw new Error(`Unreachable`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { ppath, xfs } from '@yarnpkg/fslib';
|
||||
import { valid as semverValid } from 'semver';
|
||||
import { getManifestByVersion } from '@backstage/release-manifests';
|
||||
import { BACKSTAGE_JSON, findPaths } from '@backstage/cli-common';
|
||||
import { Descriptor, structUtils } from '@yarnpkg/core';
|
||||
import { PROTOCOL } from './constants';
|
||||
|
||||
export const getCurrentBackstageVersion = () => {
|
||||
const workspaceRoot = ppath.resolve(findPaths(ppath.cwd()).targetRoot);
|
||||
|
||||
const backstageJson = xfs.readJsonSync(
|
||||
ppath.join(workspaceRoot, BACKSTAGE_JSON),
|
||||
);
|
||||
|
||||
const backstageVersion = semverValid(backstageJson.version);
|
||||
|
||||
if (backstageVersion === null) {
|
||||
throw new Error('Valid version string not found in backstage.json');
|
||||
}
|
||||
|
||||
return backstageVersion;
|
||||
};
|
||||
|
||||
export const getPackageVersion = async (descriptor: Descriptor) => {
|
||||
const ident = structUtils.stringifyIdent(descriptor);
|
||||
const range = structUtils.parseRange(descriptor.range);
|
||||
|
||||
if (range.protocol !== PROTOCOL) {
|
||||
throw new Error(`Unexpected ${range.protocol} range when packing`);
|
||||
}
|
||||
|
||||
if (!semverValid(range.selector)) {
|
||||
throw new Error(`Missing backstage version in range ${descriptor.range}`);
|
||||
}
|
||||
|
||||
const manifest = await getManifestByVersion({
|
||||
version: range.selector,
|
||||
});
|
||||
|
||||
const manifestEntry = manifest.packages.find(
|
||||
candidate => candidate.name === ident,
|
||||
);
|
||||
|
||||
if (!manifestEntry) {
|
||||
throw new Error(
|
||||
`Package ${ident} not found in manifest for Backstage v${range.selector}. ` +
|
||||
`This means the specified package is not included in this Backstage ` +
|
||||
`release. This may imply the package has been replaced with an alternative - ` +
|
||||
`please review the documentation for the package. If you need to continue ` +
|
||||
`using this package, it will be necessary to switch to manually managing its ` +
|
||||
`version.`,
|
||||
);
|
||||
}
|
||||
|
||||
return manifestEntry.version;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,8 @@
|
||||
# Auth Module: OneLogin Provider
|
||||
|
||||
This module provides an OneLogin auth provider implementation for `@backstage/plugin-auth-backend`.
|
||||
|
||||
## Links
|
||||
|
||||
- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-onelogin-provider)
|
||||
- [Backstage Project Homepage](https://backstage.io)
|
||||
@@ -0,0 +1,30 @@
|
||||
## API Report File for "@backstage/plugin-auth-backend-module-onelogin-provider"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { OAuthAuthenticator } from '@backstage/plugin-auth-node';
|
||||
import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node';
|
||||
import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node';
|
||||
import { PassportProfile } from '@backstage/plugin-auth-node';
|
||||
import { SignInResolverFactory } from '@backstage/plugin-auth-node';
|
||||
|
||||
// @public (undocumented)
|
||||
const authModuleOneLoginProvider: () => BackendFeature;
|
||||
export default authModuleOneLoginProvider;
|
||||
|
||||
// @public (undocumented)
|
||||
export const oneLoginAuthenticator: OAuthAuthenticator<
|
||||
PassportOAuthAuthenticatorHelper,
|
||||
PassportProfile
|
||||
>;
|
||||
|
||||
// @public
|
||||
export namespace oneLoginSignInResolvers {
|
||||
const usernameMatchingUserEntityName: SignInResolverFactory<
|
||||
OAuthAuthenticatorResult<PassportProfile>,
|
||||
unknown
|
||||
>;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-plugin-auth-backend-module-onelogin-provider
|
||||
title: '@backstage/plugin-auth-backend-module-onelogin-provider'
|
||||
description: The onelogin-provider backend module for the auth plugin.
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-backend-plugin-module
|
||||
owner: maintainers
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 {
|
||||
auth?: {
|
||||
providers?: {
|
||||
/** @visibility frontend */
|
||||
onelogin?: {
|
||||
[authEnv: string]: {
|
||||
clientId: string;
|
||||
/**
|
||||
* @visibility secret
|
||||
*/
|
||||
clientSecret: string;
|
||||
issuer: string;
|
||||
callbackUrl?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@backstage/plugin-auth-backend'));
|
||||
backend.add(import('../src'));
|
||||
|
||||
backend.start();
|
||||
@@ -0,0 +1,19 @@
|
||||
# Knip report
|
||||
|
||||
## Unused dependencies (4)
|
||||
|
||||
| Name | Location | Severity |
|
||||
| :---------------------------- | :----------- | :------- |
|
||||
| @backstage/backend-plugin-api | package.json | error |
|
||||
| @backstage/plugin-auth-node | package.json | error |
|
||||
| passport | package.json | error |
|
||||
| express | package.json | error |
|
||||
|
||||
## Unused devDependencies (3)
|
||||
|
||||
| Name | Location | Severity |
|
||||
| :----------------------------- | :----------- | :------- |
|
||||
| @backstage/plugin-auth-backend | package.json | error |
|
||||
| @backstage/backend-test-utils | package.json | error |
|
||||
| @backstage/backend-defaults | package.json | error |
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@backstage/plugin-auth-backend-module-onelogin-provider",
|
||||
"version": "0.0.0",
|
||||
"description": "The onelogin-provider backend module for the auth plugin.",
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/auth-backend-module-onelogin-provider"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"express": "^4.18.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-onelogin-oauth": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-defaults": "workspace:^",
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-auth-backend": "workspace:^",
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Strategy as OneLoginStrategy } from 'passport-onelogin-oauth';
|
||||
import {
|
||||
createOAuthAuthenticator,
|
||||
PassportOAuthAuthenticatorHelper,
|
||||
PassportOAuthDoneCallback,
|
||||
PassportProfile,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
|
||||
/** @public */
|
||||
export const oneLoginAuthenticator = createOAuthAuthenticator({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
const issuer = config.getString('issuer');
|
||||
|
||||
return PassportOAuthAuthenticatorHelper.from(
|
||||
new OneLoginStrategy(
|
||||
{
|
||||
clientID: clientId,
|
||||
clientSecret: clientSecret,
|
||||
callbackURL: callbackUrl,
|
||||
issuer,
|
||||
passReqToCallback: false,
|
||||
},
|
||||
(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
params: any,
|
||||
fullProfile: PassportProfile,
|
||||
done: PassportOAuthDoneCallback,
|
||||
) => {
|
||||
done(
|
||||
undefined,
|
||||
{ fullProfile, params, accessToken },
|
||||
{ refreshToken },
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
async start(input, helper) {
|
||||
input.scope = 'openid';
|
||||
return helper.start(input, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
});
|
||||
},
|
||||
|
||||
async authenticate(input, helper) {
|
||||
return helper.authenticate(input);
|
||||
},
|
||||
|
||||
async refresh(input, helper) {
|
||||
input.scope = 'openid';
|
||||
return helper.refresh(input);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The onelogin-provider backend module for the auth plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { oneLoginAuthenticator } from './authenticator';
|
||||
export { authModuleOneLoginProvider as default } from './module';
|
||||
export { oneLoginSignInResolvers } from './resolvers';
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { authModuleOneLoginProvider } from './module';
|
||||
import request from 'supertest';
|
||||
import { decodeOAuthState } from '@backstage/plugin-auth-node';
|
||||
|
||||
describe('authModuleOneLoginProvider', () => {
|
||||
it('should start', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
import('@backstage/plugin-auth-backend'),
|
||||
authModuleOneLoginProvider,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
},
|
||||
auth: {
|
||||
providers: {
|
||||
onelogin: {
|
||||
development: {
|
||||
clientId: 'my-client-id',
|
||||
clientSecret: 'my-client-secret',
|
||||
issuer: 'https://my-company.onelogin.com/oidc/2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const agent = request.agent(server);
|
||||
|
||||
const res = await agent.get('/api/auth/onelogin/start?env=development');
|
||||
|
||||
expect(res.status).toEqual(302);
|
||||
|
||||
const nonceCookie = agent.jar.getCookie('onelogin-nonce', {
|
||||
domain: 'localhost',
|
||||
path: '/api/auth/onelogin/handler',
|
||||
script: false,
|
||||
secure: false,
|
||||
});
|
||||
expect(nonceCookie).toBeDefined();
|
||||
|
||||
const startUrl = new URL(res.get('location'));
|
||||
expect(startUrl.origin).toBe('https://my-company.onelogin.com');
|
||||
expect(startUrl.pathname).toBe('/oidc/2/auth');
|
||||
expect(Object.fromEntries(startUrl.searchParams)).toEqual({
|
||||
response_type: 'code',
|
||||
scope: 'openid',
|
||||
client_id: 'my-client-id',
|
||||
redirect_uri: `http://localhost:${server.port()}/api/auth/onelogin/handler/frame`,
|
||||
state: expect.any(String),
|
||||
});
|
||||
|
||||
expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({
|
||||
env: 'development',
|
||||
nonce: decodeURIComponent(nonceCookie.value),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
authProvidersExtensionPoint,
|
||||
commonSignInResolvers,
|
||||
createOAuthProviderFactory,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { oneLoginAuthenticator } from './authenticator';
|
||||
import { oneLoginSignInResolvers } from './resolvers';
|
||||
|
||||
/** @public */
|
||||
export const authModuleOneLoginProvider = createBackendModule({
|
||||
pluginId: 'auth',
|
||||
moduleId: 'onelogin-provider',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
providers: authProvidersExtensionPoint,
|
||||
},
|
||||
async init({ providers }) {
|
||||
providers.registerProvider({
|
||||
providerId: 'onelogin',
|
||||
factory: createOAuthProviderFactory({
|
||||
authenticator: oneLoginAuthenticator,
|
||||
signInResolverFactories: {
|
||||
...oneLoginSignInResolvers,
|
||||
...commonSignInResolvers,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import {
|
||||
createSignInResolverFactory,
|
||||
OAuthAuthenticatorResult,
|
||||
PassportProfile,
|
||||
SignInInfo,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
|
||||
/**
|
||||
* Available sign-in resolvers for the OneLogin auth provider.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export namespace oneLoginSignInResolvers {
|
||||
/**
|
||||
* Looks up the user by matching their OneLogin username to the entity name.
|
||||
*/
|
||||
export const usernameMatchingUserEntityName = createSignInResolverFactory({
|
||||
create() {
|
||||
return async (
|
||||
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
ctx,
|
||||
) => {
|
||||
const { result } = info;
|
||||
|
||||
const id = result.fullProfile.username;
|
||||
if (!id) {
|
||||
throw new Error(`OneLogin user profile does not contain a username`);
|
||||
}
|
||||
|
||||
return ctx.signInWithCatalogUser({ entityRef: { name: id } });
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
+5
@@ -13,8 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare module 'passport-onelogin-oauth' {
|
||||
import { Request } from 'express';
|
||||
import { StrategyCreated } from 'passport';
|
||||
|
||||
export class Strategy {
|
||||
constructor(options: any, verify: any);
|
||||
authenticate(this: StrategyCreated<this>, req: Request, options?: any): any;
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@
|
||||
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-oidc-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-okta-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-backend-module-onelogin-provider": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
|
||||
@@ -14,156 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Strategy as OneLoginStrategy } from 'passport-onelogin-oauth';
|
||||
import express from 'express';
|
||||
import { oneLoginAuthenticator } from '@backstage/plugin-auth-backend-module-onelogin-provider';
|
||||
import {
|
||||
OAuthAdapter,
|
||||
OAuthProviderOptions,
|
||||
OAuthHandlers,
|
||||
OAuthResponse,
|
||||
OAuthEnvironmentHandler,
|
||||
OAuthStartRequest,
|
||||
encodeState,
|
||||
OAuthRefreshRequest,
|
||||
OAuthResult,
|
||||
} from '../../lib/oauth';
|
||||
import passport from 'passport';
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
executeFetchUserProfileStrategy,
|
||||
PassportDoneCallback,
|
||||
} from '../../lib/passport';
|
||||
import { OAuthStartResponse, AuthHandler } from '../types';
|
||||
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
|
||||
import {
|
||||
AuthResolverContext,
|
||||
SignInResolver,
|
||||
createOAuthProviderFactory,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export type Options = OAuthProviderOptions & {
|
||||
issuer: string;
|
||||
signInResolver?: SignInResolver<OAuthResult>;
|
||||
authHandler: AuthHandler<OAuthResult>;
|
||||
resolverContext: AuthResolverContext;
|
||||
};
|
||||
|
||||
export class OneLoginProvider implements OAuthHandlers {
|
||||
private readonly _strategy: any;
|
||||
private readonly signInResolver?: SignInResolver<OAuthResult>;
|
||||
private readonly authHandler: AuthHandler<OAuthResult>;
|
||||
private readonly resolverContext: AuthResolverContext;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.signInResolver = options.signInResolver;
|
||||
this.authHandler = options.authHandler;
|
||||
this.resolverContext = options.resolverContext;
|
||||
this._strategy = new OneLoginStrategy(
|
||||
{
|
||||
issuer: options.issuer,
|
||||
clientID: options.clientId,
|
||||
clientSecret: options.clientSecret,
|
||||
callbackURL: options.callbackUrl,
|
||||
passReqToCallback: false,
|
||||
},
|
||||
(
|
||||
accessToken: any,
|
||||
refreshToken: any,
|
||||
params: any,
|
||||
fullProfile: passport.Profile,
|
||||
done: PassportDoneCallback<OAuthResult, PrivateInfo>,
|
||||
) => {
|
||||
done(
|
||||
undefined,
|
||||
{
|
||||
accessToken,
|
||||
refreshToken,
|
||||
params,
|
||||
fullProfile,
|
||||
},
|
||||
{
|
||||
refreshToken,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
async start(req: OAuthStartRequest): Promise<OAuthStartResponse> {
|
||||
return await executeRedirectStrategy(req, this._strategy, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
scope: 'openid',
|
||||
state: encodeState(req.state),
|
||||
});
|
||||
}
|
||||
|
||||
async handler(req: express.Request) {
|
||||
const { result, privateInfo } = await executeFrameHandlerStrategy<
|
||||
OAuthResult,
|
||||
PrivateInfo
|
||||
>(req, this._strategy);
|
||||
|
||||
return {
|
||||
response: await this.handleResult(result),
|
||||
refreshToken: privateInfo.refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(req: OAuthRefreshRequest) {
|
||||
const { accessToken, refreshToken, params } =
|
||||
await executeRefreshTokenStrategy(
|
||||
this._strategy,
|
||||
req.refreshToken,
|
||||
'openid',
|
||||
);
|
||||
|
||||
const fullProfile = await executeFetchUserProfileStrategy(
|
||||
this._strategy,
|
||||
accessToken,
|
||||
);
|
||||
|
||||
return {
|
||||
response: await this.handleResult({
|
||||
fullProfile,
|
||||
params,
|
||||
accessToken,
|
||||
}),
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
private async handleResult(result: OAuthResult) {
|
||||
const { profile } = await this.authHandler(result, this.resolverContext);
|
||||
|
||||
const response: OAuthResponse = {
|
||||
providerInfo: {
|
||||
idToken: result.params.id_token,
|
||||
accessToken: result.accessToken,
|
||||
scope: result.params.scope,
|
||||
expiresInSeconds: result.params.expires_in,
|
||||
},
|
||||
profile,
|
||||
};
|
||||
|
||||
if (this.signInResolver) {
|
||||
response.backstageIdentity = await this.signInResolver(
|
||||
{
|
||||
result,
|
||||
profile,
|
||||
},
|
||||
this.resolverContext,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
import {
|
||||
adaptLegacyOAuthHandler,
|
||||
adaptLegacyOAuthSignInResolver,
|
||||
} from '../../lib/legacy';
|
||||
import { OAuthResult } from '../../lib/oauth';
|
||||
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
|
||||
import { AuthHandler } from '../types';
|
||||
|
||||
/**
|
||||
* Auth provider integration for OneLogin auth
|
||||
@@ -188,36 +50,10 @@ export const onelogin = createAuthProviderIntegration({
|
||||
resolver: SignInResolver<OAuthResult>;
|
||||
};
|
||||
}) {
|
||||
return ({ providerId, globalConfig, config, resolverContext }) =>
|
||||
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
||||
const clientId = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const issuer = envConfig.getString('issuer');
|
||||
const customCallbackUrl = envConfig.getOptionalString('callbackUrl');
|
||||
const callbackUrl =
|
||||
customCallbackUrl ||
|
||||
`${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
||||
|
||||
const authHandler: AuthHandler<OAuthResult> = options?.authHandler
|
||||
? options.authHandler
|
||||
: async ({ fullProfile, params }) => ({
|
||||
profile: makeProfileInfo(fullProfile, params.id_token),
|
||||
});
|
||||
|
||||
const provider = new OneLoginProvider({
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
issuer,
|
||||
authHandler,
|
||||
signInResolver: options?.signIn?.resolver,
|
||||
resolverContext,
|
||||
});
|
||||
|
||||
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
||||
providerId,
|
||||
callbackUrl,
|
||||
});
|
||||
});
|
||||
return createOAuthProviderFactory({
|
||||
authenticator: oneLoginAuthenticator,
|
||||
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
||||
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -115,6 +115,7 @@ export type EntityRelationsGraphProps = {
|
||||
mergeRelations?: boolean;
|
||||
kinds?: string[];
|
||||
relations?: string[];
|
||||
entityFilter?: (entity: Entity) => boolean;
|
||||
direction?: Direction;
|
||||
onNodeClick?: (value: EntityNode, event: MouseEvent_2<unknown>) => void;
|
||||
relationPairs?: RelationPairs;
|
||||
|
||||
@@ -71,6 +71,7 @@ export const CatalogGraphCard = (
|
||||
direction = Direction.LEFT_RIGHT,
|
||||
kinds,
|
||||
relations,
|
||||
entityFilter,
|
||||
height,
|
||||
className,
|
||||
rootEntityNames,
|
||||
@@ -140,6 +141,7 @@ export const CatalogGraphCard = (
|
||||
mergeRelations={mergeRelations}
|
||||
direction={direction}
|
||||
relationPairs={relationPairs}
|
||||
entityFilter={entityFilter}
|
||||
zoom={zoom}
|
||||
/>
|
||||
</InfoCard>
|
||||
|
||||
@@ -121,7 +121,11 @@ export const CatalogGraphPage = (
|
||||
};
|
||||
} & Partial<EntityRelationsGraphProps>,
|
||||
) => {
|
||||
const { relationPairs = ALL_RELATION_PAIRS, initialState } = props;
|
||||
const {
|
||||
relationPairs = ALL_RELATION_PAIRS,
|
||||
initialState,
|
||||
entityFilter,
|
||||
} = props;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const classes = useStyles();
|
||||
@@ -256,6 +260,7 @@ export const CatalogGraphPage = (
|
||||
onNodeClick={onNodeClick}
|
||||
direction={direction}
|
||||
relationPairs={relationPairs}
|
||||
entityFilter={entityFilter}
|
||||
className={classes.graph}
|
||||
zoom="enabled"
|
||||
curve={curve}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import {
|
||||
CompoundEntityRef,
|
||||
Entity,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
@@ -77,6 +78,7 @@ export type EntityRelationsGraphProps = {
|
||||
mergeRelations?: boolean;
|
||||
kinds?: string[];
|
||||
relations?: string[];
|
||||
entityFilter?: (entity: Entity) => boolean;
|
||||
direction?: Direction;
|
||||
onNodeClick?: (value: EntityNode, event: MouseEvent<unknown>) => void;
|
||||
relationPairs?: RelationPairs;
|
||||
@@ -101,6 +103,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => {
|
||||
mergeRelations = true,
|
||||
kinds,
|
||||
relations,
|
||||
entityFilter,
|
||||
direction = Direction.LEFT_RIGHT,
|
||||
onNodeClick,
|
||||
relationPairs = ALL_RELATION_PAIRS,
|
||||
@@ -130,6 +133,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => {
|
||||
mergeRelations,
|
||||
kinds,
|
||||
relations,
|
||||
entityFilter,
|
||||
onNodeClick,
|
||||
relationPairs,
|
||||
});
|
||||
|
||||
+22
@@ -353,6 +353,28 @@ describe('useEntityRelationGraph', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should filter by func', async () => {
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useEntityRelationGraph({
|
||||
rootEntityRefs: ['b:d/c'],
|
||||
filter: {
|
||||
entityFilter: e => e.metadata.name !== 'c2',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Simulate rerendering as this is triggered automatically due to the mock
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
rerender();
|
||||
}
|
||||
|
||||
expect(result.current.entities).toEqual({
|
||||
'b:d/c': expect.anything(),
|
||||
'b:d/c1': expect.anything(),
|
||||
'k:d/a1': expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
test('should support multiple roots by kind', async () => {
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useEntityRelationGraph({
|
||||
|
||||
+27
-4
@@ -16,6 +16,7 @@
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useEffect } from 'react';
|
||||
import { useEntityStore } from './useEntityStore';
|
||||
import { pickBy } from 'lodash';
|
||||
|
||||
/**
|
||||
* Discover the graph of entities connected by relations, starting from a set of
|
||||
@@ -24,13 +25,19 @@ import { useEntityStore } from './useEntityStore';
|
||||
*/
|
||||
export function useEntityRelationGraph({
|
||||
rootEntityRefs,
|
||||
filter: { maxDepth = Number.POSITIVE_INFINITY, relations, kinds } = {},
|
||||
filter: {
|
||||
maxDepth = Number.POSITIVE_INFINITY,
|
||||
relations,
|
||||
kinds,
|
||||
entityFilter,
|
||||
} = {},
|
||||
}: {
|
||||
rootEntityRefs: string[];
|
||||
filter?: {
|
||||
maxDepth?: number;
|
||||
relations?: string[];
|
||||
kinds?: string[];
|
||||
entityFilter?: (entity: Entity) => boolean;
|
||||
};
|
||||
}): {
|
||||
entities?: { [ref: string]: Entity };
|
||||
@@ -60,6 +67,11 @@ export function useEntityRelationGraph({
|
||||
processedEntityRefs.add(entityRef);
|
||||
|
||||
if (entity && entity.relations) {
|
||||
// If the entity is filtered out then no need to check any
|
||||
// of its outgoing relationships to other entities
|
||||
if (entityFilter && !entityFilter(entity)) {
|
||||
continue;
|
||||
}
|
||||
for (const rel of entity.relations) {
|
||||
if (
|
||||
(!relations || relations.includes(rel.type)) &&
|
||||
@@ -81,12 +93,23 @@ export function useEntityRelationGraph({
|
||||
|
||||
++depth;
|
||||
}
|
||||
|
||||
requestEntities([...expectedEntities]);
|
||||
}, [entities, rootEntityRefs, maxDepth, relations, kinds, requestEntities]);
|
||||
}, [
|
||||
entities,
|
||||
rootEntityRefs,
|
||||
maxDepth,
|
||||
relations,
|
||||
kinds,
|
||||
entityFilter,
|
||||
requestEntities,
|
||||
]);
|
||||
|
||||
const filteredEntities = entityFilter
|
||||
? pickBy(entities, (value, _key) => entityFilter(value))
|
||||
: entities;
|
||||
|
||||
return {
|
||||
entities,
|
||||
entities: filteredEntities,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
+4
-1
@@ -18,7 +18,7 @@ import useDebounce from 'react-use/esm/useDebounce';
|
||||
import { RelationPairs, ALL_RELATION_PAIRS } from './relations';
|
||||
import { EntityEdge, EntityNode } from './types';
|
||||
import { useEntityRelationGraph } from './useEntityRelationGraph';
|
||||
import { DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
|
||||
/**
|
||||
* Generate nodes and edges to render the entity graph.
|
||||
@@ -30,6 +30,7 @@ export function useEntityRelationNodesAndEdges({
|
||||
mergeRelations = true,
|
||||
kinds,
|
||||
relations,
|
||||
entityFilter,
|
||||
onNodeClick,
|
||||
relationPairs = ALL_RELATION_PAIRS,
|
||||
}: {
|
||||
@@ -39,6 +40,7 @@ export function useEntityRelationNodesAndEdges({
|
||||
mergeRelations?: boolean;
|
||||
kinds?: string[];
|
||||
relations?: string[];
|
||||
entityFilter?: (entity: Entity) => boolean;
|
||||
onNodeClick?: (value: EntityNode, event: MouseEvent<unknown>) => void;
|
||||
relationPairs?: RelationPairs;
|
||||
}): {
|
||||
@@ -57,6 +59,7 @@ export function useEntityRelationNodesAndEdges({
|
||||
maxDepth,
|
||||
kinds,
|
||||
relations,
|
||||
entityFilter,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+4
-1
@@ -20,7 +20,7 @@ import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState, ReactNode } from 'react';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
import { catalogApiRef } from '../../api';
|
||||
@@ -152,6 +152,9 @@ export function EntityAutocompletePicker<
|
||||
<Typography className={classes.label} variant="button" component="label">
|
||||
{label}
|
||||
<Autocomplete<string, true>
|
||||
PopperComponent={popperProps => (
|
||||
<div {...popperProps}>{popperProps.children as ReactNode}</div>
|
||||
)}
|
||||
multiple
|
||||
disableCloseOnSelect
|
||||
options={availableOptions}
|
||||
|
||||
@@ -30,7 +30,7 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox';
|
||||
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState, ReactNode } from 'react';
|
||||
import { useEntityList } from '../../hooks/useEntityListProvider';
|
||||
import { EntityOwnerFilter } from '../../filters';
|
||||
import { useDebouncedEffect } from '@react-hookz/web';
|
||||
@@ -178,6 +178,9 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => {
|
||||
<Typography className={classes.label} variant="button" component="label">
|
||||
Owner
|
||||
<Autocomplete
|
||||
PopperComponent={popperProps => (
|
||||
<div {...popperProps}>{popperProps.children as ReactNode}</div>
|
||||
)}
|
||||
multiple
|
||||
disableCloseOnSelect
|
||||
loading={loading}
|
||||
|
||||
+4
-1
@@ -24,7 +24,7 @@ import { makeStyles } from '@material-ui/core/styles';
|
||||
import CheckBoxIcon from '@material-ui/icons/CheckBox';
|
||||
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, ReactNode } from 'react';
|
||||
import { useEntityList } from '../../hooks';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
|
||||
@@ -73,6 +73,9 @@ export const EntityProcessingStatusPicker = () => {
|
||||
<Typography className={classes.label} variant="button" component="label">
|
||||
Processing Status
|
||||
<Autocomplete<string, true>
|
||||
PopperComponent={popperProps => (
|
||||
<div {...popperProps}>{popperProps.children as ReactNode}</div>
|
||||
)}
|
||||
multiple
|
||||
disableCloseOnSelect
|
||||
options={availableAdvancedItems}
|
||||
|
||||
@@ -29,7 +29,8 @@ type PaginatedCatalogTableProps = {
|
||||
* @internal
|
||||
*/
|
||||
export function PaginatedCatalogTable(props: PaginatedCatalogTableProps) {
|
||||
const { columns, data, next, prev, title, isLoading, options } = props;
|
||||
const { columns, data, next, prev, title, isLoading, options, ...restProps } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Table
|
||||
@@ -60,6 +61,7 @@ export function PaginatedCatalogTable(props: PaginatedCatalogTableProps) {
|
||||
/* this will enable the next button accordingly */
|
||||
totalCount={next ? Number.MAX_VALUE : Number.MAX_SAFE_INTEGER}
|
||||
localization={{ pagination: { labelDisplayedRows: '' } }}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('debug:wait', () => {
|
||||
await expect(async () => {
|
||||
await action.handler(context);
|
||||
}).rejects.toThrow(
|
||||
'Waiting duration is longer than the maximum threshold of 0 hours, 0 minutes, 30 seconds',
|
||||
'Waiting duration is longer than the maximum threshold of 0 hours, 10 minutes, 0 seconds',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ import { examples } from './wait.examples';
|
||||
|
||||
const id = 'debug:wait';
|
||||
|
||||
const MAX_WAIT_TIME_IN_ISO = 'T00:00:30';
|
||||
const MAX_WAIT_TIME_IN_ISO = 'T00:10:00';
|
||||
|
||||
/**
|
||||
* Waits for a certain period of time.
|
||||
|
||||
+4
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { Progress } from '@backstage/core-components';
|
||||
import Box from '@material-ui/core/Box';
|
||||
@@ -78,6 +78,9 @@ export const TemplateCategoryPicker = () => {
|
||||
Categories
|
||||
</Typography>
|
||||
<Autocomplete<string, true>
|
||||
PopperComponent={popperProps => (
|
||||
<div {...popperProps}>{popperProps.children as ReactNode}</div>
|
||||
)}
|
||||
multiple
|
||||
id="categories-picker"
|
||||
options={availableTypes}
|
||||
|
||||
@@ -16,14 +16,18 @@
|
||||
|
||||
import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
CatalogApi,
|
||||
catalogApiRef,
|
||||
entityPresentationApiRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { EntityPicker } from './EntityPicker';
|
||||
import { EntityPickerProps } from './schema';
|
||||
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
|
||||
import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog';
|
||||
|
||||
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
|
||||
apiVersion: 'scaffolder.backstage.io/v1beta3',
|
||||
@@ -50,6 +54,7 @@ describe('<EntityPicker />', () => {
|
||||
getLocationByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
} as any;
|
||||
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -59,7 +64,15 @@ describe('<EntityPicker />', () => {
|
||||
];
|
||||
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, catalogApi],
|
||||
[
|
||||
entityPresentationApiRef,
|
||||
DefaultEntityPresentationApi.create({ catalogApi }),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
@@ -90,7 +103,12 @@ describe('<EntityPicker />', () => {
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
fields: ['metadata.name', 'metadata.namespace', 'kind'],
|
||||
fields: [
|
||||
'metadata.name',
|
||||
'metadata.namespace',
|
||||
'metadata.title',
|
||||
'kind',
|
||||
],
|
||||
filter: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,13 +24,16 @@ import {
|
||||
} from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
EntityDisplayName,
|
||||
EntityRefPresentationSnapshot,
|
||||
catalogApiRef,
|
||||
humanizeEntityRef,
|
||||
entityPresentationApiRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import Autocomplete, {
|
||||
AutocompleteChangeReason,
|
||||
createFilterOptions,
|
||||
} from '@material-ui/lab/Autocomplete';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
@@ -65,31 +68,55 @@ export const EntityPicker = (props: EntityPickerProps) => {
|
||||
uiSchema['ui:options']?.defaultNamespace || undefined;
|
||||
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const entityPresentationApi = useApi(entityPresentationApiRef);
|
||||
|
||||
const { value: entities, loading } = useAsync(async () => {
|
||||
const fields = ['metadata.name', 'metadata.namespace', 'kind'];
|
||||
const fields = [
|
||||
'metadata.name',
|
||||
'metadata.namespace',
|
||||
'metadata.title',
|
||||
'kind',
|
||||
];
|
||||
const { items } = await catalogApi.getEntities(
|
||||
catalogFilter
|
||||
? { filter: catalogFilter, fields }
|
||||
: { filter: undefined, fields },
|
||||
);
|
||||
return items;
|
||||
|
||||
const entityRefToPresentation = new Map<
|
||||
string,
|
||||
EntityRefPresentationSnapshot
|
||||
>(
|
||||
await Promise.all(
|
||||
items.map(async item => {
|
||||
const presentation = await entityPresentationApi.forEntity(item)
|
||||
.promise;
|
||||
return [stringifyEntityRef(item), presentation] as [
|
||||
string,
|
||||
EntityRefPresentationSnapshot,
|
||||
];
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return { catalogEntities: items, entityRefToPresentation };
|
||||
});
|
||||
|
||||
const allowArbitraryValues =
|
||||
uiSchema['ui:options']?.allowArbitraryValues ?? true;
|
||||
|
||||
const getLabel = useCallback(
|
||||
(ref: string) => {
|
||||
(freeSoloValue: string) => {
|
||||
try {
|
||||
return humanizeEntityRef(
|
||||
parseEntityRef(ref, { defaultKind, defaultNamespace }),
|
||||
{
|
||||
defaultKind,
|
||||
defaultNamespace,
|
||||
},
|
||||
);
|
||||
// Will throw if defaultKind or defaultNamespace are not set
|
||||
const parsedRef = parseEntityRef(freeSoloValue, {
|
||||
defaultKind,
|
||||
defaultNamespace,
|
||||
});
|
||||
|
||||
return stringifyEntityRef(parsedRef);
|
||||
} catch (err) {
|
||||
return ref;
|
||||
return freeSoloValue;
|
||||
}
|
||||
},
|
||||
[defaultKind, defaultNamespace],
|
||||
@@ -129,12 +156,12 @@ export const EntityPicker = (props: EntityPickerProps) => {
|
||||
// Since free solo can be enabled, attempt to parse as a full entity ref first, then fall
|
||||
// back to the given value.
|
||||
const selectedEntity =
|
||||
entities?.find(e => stringifyEntityRef(e) === formData) ??
|
||||
entities?.catalogEntities.find(e => stringifyEntityRef(e) === formData) ??
|
||||
(allowArbitraryValues && formData ? getLabel(formData) : '');
|
||||
|
||||
useEffect(() => {
|
||||
if (entities?.length === 1 && selectedEntity === '') {
|
||||
onChange(stringifyEntityRef(entities[0]));
|
||||
if (entities?.catalogEntities.length === 1 && selectedEntity === '') {
|
||||
onChange(stringifyEntityRef(entities.catalogEntities[0]));
|
||||
}
|
||||
}, [entities, onChange, selectedEntity]);
|
||||
|
||||
@@ -145,17 +172,18 @@ export const EntityPicker = (props: EntityPickerProps) => {
|
||||
error={rawErrors?.length > 0 && !formData}
|
||||
>
|
||||
<Autocomplete
|
||||
disabled={entities?.length === 1}
|
||||
disabled={entities?.catalogEntities.length === 1}
|
||||
id={idSchema?.$id}
|
||||
value={selectedEntity}
|
||||
loading={loading}
|
||||
onChange={onSelect}
|
||||
options={entities || []}
|
||||
options={entities?.catalogEntities || []}
|
||||
getOptionLabel={option =>
|
||||
// option can be a string due to freeSolo.
|
||||
typeof option === 'string'
|
||||
? option
|
||||
: humanizeEntityRef(option, { defaultKind, defaultNamespace })!
|
||||
: entities?.entityRefToPresentation.get(stringifyEntityRef(option))
|
||||
?.entityRef!
|
||||
}
|
||||
autoSelect
|
||||
freeSolo={allowArbitraryValues}
|
||||
@@ -171,6 +199,12 @@ export const EntityPicker = (props: EntityPickerProps) => {
|
||||
InputProps={params.InputProps}
|
||||
/>
|
||||
)}
|
||||
renderOption={option => <EntityDisplayName entityRef={option} />}
|
||||
filterOptions={createFilterOptions<Entity>({
|
||||
stringify: option =>
|
||||
entities?.entityRefToPresentation.get(stringifyEntityRef(option))
|
||||
?.primaryTitle!,
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
@@ -16,11 +16,16 @@
|
||||
|
||||
import { type EntityFilterQuery } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
CatalogApi,
|
||||
catalogApiRef,
|
||||
entityPresentationApiRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
|
||||
import React from 'react';
|
||||
import { OwnerPicker } from './OwnerPicker';
|
||||
import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog';
|
||||
|
||||
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
@@ -64,7 +69,15 @@ describe('<OwnerPicker />', () => {
|
||||
];
|
||||
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, catalogApi],
|
||||
[
|
||||
entityPresentationApiRef,
|
||||
DefaultEntityPresentationApi.create({ catalogApi }),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
@@ -99,7 +112,12 @@ describe('<OwnerPicker />', () => {
|
||||
filter: {
|
||||
kind: ['Group', 'User'],
|
||||
},
|
||||
fields: ['metadata.name', 'metadata.namespace', 'kind'],
|
||||
fields: [
|
||||
'metadata.name',
|
||||
'metadata.namespace',
|
||||
'metadata.title',
|
||||
'kind',
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -132,7 +150,12 @@ describe('<OwnerPicker />', () => {
|
||||
filter: {
|
||||
kind: ['User'],
|
||||
},
|
||||
fields: ['metadata.name', 'metadata.namespace', 'kind'],
|
||||
fields: [
|
||||
'metadata.name',
|
||||
'metadata.namespace',
|
||||
'metadata.title',
|
||||
'kind',
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,13 +6,26 @@ authorization token.
|
||||
|
||||
## Setup backend
|
||||
|
||||
1. Install the backend plugin:
|
||||
Install the backend plugin
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn --cwd packages/backend add @backstage/plugin-user-settings-backend
|
||||
```
|
||||
|
||||
### New backend
|
||||
|
||||
Add the plugin to your backend in `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
backend.add(import('@backstage/plugin-user-settings-backend/alpha'));
|
||||
```
|
||||
|
||||
To get real-time updates of the user settings across different user sessions, you must also install
|
||||
the `@backstage/plugin-signals-backend` plugin.
|
||||
|
||||
### Old backend
|
||||
|
||||
1. Configure the routes by adding a new `userSettings.ts` file in
|
||||
`packages/backend/src/plugins/`:
|
||||
|
||||
@@ -29,7 +42,7 @@ export default async function createPlugin(env: PluginEnvironment) {
|
||||
}
|
||||
```
|
||||
|
||||
3. Add the new routes to your backend by modifying `packages/backend/src/index.ts`:
|
||||
2. Add the new routes to your backend by modifying `packages/backend/src/index.ts`:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.ts
|
||||
@@ -58,6 +71,7 @@ To make use of the user settings backend, replace the `WebStorage` with the
|
||||
+ storageApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
+import { UserSettingsStorage } from '@backstage/plugin-user-settings';
|
||||
+import { signalApiRef } from '@backstage/plugin-signals-react';
|
||||
|
||||
export const apis: AnyApiFactory[] = [
|
||||
+ createApiFactory({
|
||||
@@ -66,7 +80,8 @@ To make use of the user settings backend, replace the `WebStorage` with the
|
||||
+ discoveryApi: discoveryApiRef,
|
||||
+ errorApi: errorApiRef,
|
||||
+ fetchApi: fetchApiRef,
|
||||
+ identityApi: identityApiRef
|
||||
+ identityApi: identityApiRef,
|
||||
+ signalApi: signalApiRef, // Optional
|
||||
+ },
|
||||
+ factory: deps => UserSettingsStorage.create(deps),
|
||||
+ }),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import express from 'express';
|
||||
import { IdentityApi } from '@backstage/plugin-auth-node';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
import { SignalsService } from '@backstage/plugin-signals-node';
|
||||
|
||||
// @public
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
@@ -16,6 +17,8 @@ export interface RouterOptions {
|
||||
database: PluginDatabaseManager;
|
||||
// (undocumented)
|
||||
identity: IdentityApi;
|
||||
// (undocumented)
|
||||
signals?: SignalsService;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/plugin-signals-node": "workspace:^",
|
||||
"@backstage/plugin-user-settings-common": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@types/express": "^4.17.6",
|
||||
"express": "^4.17.1",
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createBackendPlugin,
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createRouter } from './service/router';
|
||||
import { signalsServiceRef } from '@backstage/plugin-signals-node';
|
||||
|
||||
/**
|
||||
* The user settings backend plugin.
|
||||
@@ -33,9 +34,10 @@ export default createBackendPlugin({
|
||||
database: coreServices.database,
|
||||
identity: coreServices.identity,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
signals: signalsServiceRef,
|
||||
},
|
||||
async init({ database, identity, httpRouter }) {
|
||||
httpRouter.use(await createRouter({ database, identity }));
|
||||
async init({ database, identity, httpRouter, signals }) {
|
||||
httpRouter.use(await createRouter({ database, identity, signals }));
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@ import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { UserSettingsStore } from '../database/UserSettingsStore';
|
||||
import { createRouterInternal } from './router';
|
||||
import { SignalsService } from '@backstage/plugin-signals-node';
|
||||
|
||||
describe('createRouter', () => {
|
||||
const userSettingsStore: jest.Mocked<UserSettingsStore> = {
|
||||
@@ -36,6 +37,9 @@ describe('createRouter', () => {
|
||||
const identityApi: jest.Mocked<Partial<IdentityApi>> = {
|
||||
getIdentity: getIdentityMock,
|
||||
};
|
||||
const signalService: jest.Mocked<SignalsService> = {
|
||||
publish: jest.fn(),
|
||||
};
|
||||
|
||||
let app: express.Express;
|
||||
|
||||
@@ -43,6 +47,7 @@ describe('createRouter', () => {
|
||||
const router = await createRouterInternal({
|
||||
userSettingsStore,
|
||||
identity: identityApi as IdentityApi,
|
||||
signals: signalService as SignalsService,
|
||||
});
|
||||
|
||||
app = express().use(router);
|
||||
@@ -118,6 +123,11 @@ describe('createRouter', () => {
|
||||
bucket: 'my-bucket',
|
||||
key: 'my-key',
|
||||
});
|
||||
expect(signalService.publish).toHaveBeenCalledWith({
|
||||
recipients: { type: 'user', entityRef: 'user-1' },
|
||||
channel: `user-settings`,
|
||||
message: { type: 'key-deleted', key: 'my-key' },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if the Authorization header is missing', async () => {
|
||||
@@ -167,6 +177,11 @@ describe('createRouter', () => {
|
||||
bucket: 'my-bucket',
|
||||
key: 'my-key',
|
||||
});
|
||||
expect(signalService.publish).toHaveBeenCalledWith({
|
||||
recipients: { type: 'user', entityRef: 'user-1' },
|
||||
channel: `user-settings`,
|
||||
message: { type: 'key-changed', key: 'my-key' },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if the value is not given', async () => {
|
||||
|
||||
@@ -21,6 +21,8 @@ import express, { Request } from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { DatabaseUserSettingsStore } from '../database/DatabaseUserSettingsStore';
|
||||
import { UserSettingsStore } from '../database/UserSettingsStore';
|
||||
import { SignalsService } from '@backstage/plugin-signals-node';
|
||||
import { UserSettingsSignal } from '@backstage/plugin-user-settings-common';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -28,6 +30,7 @@ import { UserSettingsStore } from '../database/UserSettingsStore';
|
||||
export interface RouterOptions {
|
||||
database: PluginDatabaseManager;
|
||||
identity: IdentityApi;
|
||||
signals?: SignalsService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,12 +48,14 @@ export async function createRouter(
|
||||
return await createRouterInternal({
|
||||
userSettingsStore,
|
||||
identity: options.identity,
|
||||
signals: options.signals,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createRouterInternal(options: {
|
||||
identity: IdentityApi;
|
||||
userSettingsStore: UserSettingsStore;
|
||||
signals?: SignalsService;
|
||||
}): Promise<express.Router> {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
@@ -104,6 +109,14 @@ export async function createRouterInternal(options: {
|
||||
key,
|
||||
});
|
||||
|
||||
if (options.signals) {
|
||||
await options.signals.publish<UserSettingsSignal>({
|
||||
recipients: { type: 'user', entityRef: userEntityRef },
|
||||
channel: `user-settings`,
|
||||
message: { type: 'key-changed', key },
|
||||
});
|
||||
}
|
||||
|
||||
res.json(setting);
|
||||
});
|
||||
|
||||
@@ -113,6 +126,13 @@ export async function createRouterInternal(options: {
|
||||
const { bucket, key } = req.params;
|
||||
|
||||
await options.userSettingsStore.delete({ userEntityRef, bucket, key });
|
||||
if (options.signals) {
|
||||
await options.signals.publish<UserSettingsSignal>({
|
||||
recipients: { type: 'user', entityRef: userEntityRef },
|
||||
channel: 'user-settings',
|
||||
message: { type: 'key-deleted', key },
|
||||
});
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# @backstage/plugin-user-settings-common
|
||||
|
||||
Welcome to the common package for the user-settings plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,13 @@
|
||||
## API Report File for "@backstage/plugin-user-settings-common"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
// @public (undocumented)
|
||||
export type UserSettingsSignal = {
|
||||
type: 'key-changed' | 'key-deleted';
|
||||
key: string;
|
||||
};
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-plugin-user-settings-common
|
||||
title: '@backstage/plugin-user-settings-common'
|
||||
description: The Backstage common plugin to manage user settings
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-common-library
|
||||
owner: maintainers
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@backstage/plugin-user-settings-common",
|
||||
"version": "0.0.0",
|
||||
"description": "Common functionalities for the user-settings plugin",
|
||||
"backstage": {
|
||||
"role": "common-library"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/user-settings-common"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"sideEffects": false,
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -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 './types';
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 {};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user