Merge branch 'backstage:master' into tech-docs-search-by-title

This commit is contained in:
Stephen
2024-06-24 09:14:15 -04:00
committed by GitHub
78 changed files with 1205 additions and 129 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/backend-plugin-api': patch
'@backstage/backend-defaults': patch
---
Added a new Root Health Service which adds new endpoints for health checks.
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-catalog-backend-module-ldap': patch
'@backstage/backend-plugin-api': patch
'@backstage/backend-defaults': patch
'@backstage/backend-tasks': patch
---
Fix bug where ISO durations could no longer be used for schedules
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/cli': patch
---
- remove unused dependencies `winston` and `yn` from the template of backend plugins;
- update `msw` to version `2.3.1` in the template of backend plugins;
starting with v1 and switching later to v2 is tedious and not straight forward; it's easier to start with v2;
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Updated the scaffolding output message for `plugin-common` in `backstage-cli`. Now, when executing `backstage-cli new` to create a new `plugin-common` package, the output message accurately reflects the action by displaying `Creating common plugin package...` instead of the previous, less accurate `Creating backend plugin...`.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-org': patch
---
Added `relationType` property to EntityMembersListCard component that allows for display users related to a group via some other relationship aside from `memberOf`.
Also, as a side effect, the `relationsType` property has been deprecated in favor of a more accurately named `relationAggregation` property.
+190
View File
@@ -0,0 +1,190 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"example-app": "0.2.98",
"@backstage/app-defaults": "1.5.6",
"example-app-next": "0.0.12",
"app-next-example-plugin": "0.0.12",
"example-backend": "0.0.27",
"@backstage/backend-app-api": "0.7.6",
"@backstage/backend-common": "0.23.0",
"@backstage/backend-defaults": "0.3.0",
"@backstage/backend-dev-utils": "0.1.4",
"@backstage/backend-dynamic-feature-service": "0.2.11",
"example-backend-legacy": "0.2.99",
"@backstage/backend-openapi-utils": "0.1.12",
"@backstage/backend-plugin-api": "0.6.19",
"@backstage/backend-tasks": "0.5.24",
"@backstage/backend-test-utils": "0.4.0",
"@backstage/catalog-client": "1.6.5",
"@backstage/catalog-model": "1.5.0",
"@backstage/cli": "0.26.7",
"@backstage/cli-common": "0.1.14",
"@backstage/cli-node": "0.2.6",
"@backstage/codemods": "0.1.49",
"@backstage/config": "1.2.0",
"@backstage/config-loader": "1.8.1",
"@backstage/core-app-api": "1.12.6",
"@backstage/core-compat-api": "0.2.6",
"@backstage/core-components": "0.14.8",
"@backstage/core-plugin-api": "1.9.3",
"@backstage/create-app": "0.5.16",
"@backstage/dev-utils": "1.0.33",
"e2e-test": "0.2.17",
"@backstage/e2e-test-utils": "0.1.1",
"@backstage/errors": "1.2.4",
"@backstage/eslint-plugin": "0.1.8",
"@backstage/frontend-app-api": "0.7.1",
"@backstage/frontend-plugin-api": "0.6.6",
"@backstage/frontend-test-utils": "0.1.8",
"@backstage/integration": "1.12.0",
"@backstage/integration-aws-node": "0.1.12",
"@backstage/integration-react": "1.1.28",
"@backstage/release-manifests": "0.0.11",
"@backstage/repo-tools": "0.9.1",
"@techdocs/cli": "1.8.12",
"techdocs-cli-embedded-app": "0.2.97",
"@backstage/test-utils": "1.5.6",
"@backstage/theme": "0.5.6",
"@backstage/types": "1.1.1",
"@backstage/version-bridge": "1.0.8",
"yarn-plugin-backstage": "0.0.1",
"@backstage/plugin-api-docs": "0.11.6",
"@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.7",
"@backstage/plugin-app-backend": "0.3.68",
"@backstage/plugin-app-node": "0.1.19",
"@backstage/plugin-app-visualizer": "0.1.7",
"@backstage/plugin-auth-backend": "0.22.6",
"@backstage/plugin-auth-backend-module-atlassian-provider": "0.2.0",
"@backstage/plugin-auth-backend-module-aws-alb-provider": "0.1.11",
"@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.1.2",
"@backstage/plugin-auth-backend-module-bitbucket-provider": "0.1.2",
"@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.1.2",
"@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.2.14",
"@backstage/plugin-auth-backend-module-github-provider": "0.1.16",
"@backstage/plugin-auth-backend-module-gitlab-provider": "0.1.16",
"@backstage/plugin-auth-backend-module-google-provider": "0.1.16",
"@backstage/plugin-auth-backend-module-guest-provider": "0.1.5",
"@backstage/plugin-auth-backend-module-microsoft-provider": "0.1.14",
"@backstage/plugin-auth-backend-module-oauth2-provider": "0.2.0",
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.1.12",
"@backstage/plugin-auth-backend-module-oidc-provider": "0.2.0",
"@backstage/plugin-auth-backend-module-okta-provider": "0.0.12",
"@backstage/plugin-auth-backend-module-onelogin-provider": "0.1.0",
"@backstage/plugin-auth-backend-module-pinniped-provider": "0.1.13",
"@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.2.0",
"@backstage/plugin-auth-node": "0.4.14",
"@backstage/plugin-auth-react": "0.1.3",
"@backstage/plugin-bitbucket-cloud-common": "0.2.20",
"@backstage/plugin-catalog": "1.21.0",
"@backstage/plugin-catalog-backend": "1.23.0",
"@backstage/plugin-catalog-backend-module-aws": "0.3.14",
"@backstage/plugin-catalog-backend-module-azure": "0.1.39",
"@backstage/plugin-catalog-backend-module-backstage-openapi": "0.2.2",
"@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.2.6",
"@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.33",
"@backstage/plugin-catalog-backend-module-gcp": "0.1.20",
"@backstage/plugin-catalog-backend-module-gerrit": "0.1.36",
"@backstage/plugin-catalog-backend-module-github": "0.6.2",
"@backstage/plugin-catalog-backend-module-github-org": "0.1.14",
"@backstage/plugin-catalog-backend-module-gitlab": "0.3.18",
"@backstage/plugin-catalog-backend-module-gitlab-org": "0.0.2",
"@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.4.24",
"@backstage/plugin-catalog-backend-module-ldap": "0.6.0",
"@backstage/plugin-catalog-backend-module-msgraph": "0.5.27",
"@backstage/plugin-catalog-backend-module-openapi": "0.1.37",
"@backstage/plugin-catalog-backend-module-puppetdb": "0.1.25",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.1.17",
"@backstage/plugin-catalog-backend-module-unprocessed": "0.4.6",
"@backstage/plugin-catalog-common": "1.0.24",
"@backstage/plugin-catalog-graph": "0.4.6",
"@backstage/plugin-catalog-import": "0.12.0",
"@backstage/plugin-catalog-node": "1.12.1",
"@backstage/plugin-catalog-react": "1.12.1",
"@backstage/plugin-catalog-unprocessed-entities": "0.2.5",
"@backstage/plugin-catalog-unprocessed-entities-common": "0.0.2",
"@backstage/plugin-config-schema": "0.1.56",
"@backstage/plugin-devtools": "0.1.15",
"@backstage/plugin-devtools-backend": "0.3.5",
"@backstage/plugin-devtools-common": "0.1.10",
"@backstage/plugin-events-backend": "0.3.6",
"@backstage/plugin-events-backend-module-aws-sqs": "0.3.5",
"@backstage/plugin-events-backend-module-azure": "0.2.5",
"@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.5",
"@backstage/plugin-events-backend-module-gerrit": "0.2.5",
"@backstage/plugin-events-backend-module-github": "0.2.5",
"@backstage/plugin-events-backend-module-gitlab": "0.2.5",
"@backstage/plugin-events-backend-test-utils": "0.1.29",
"@backstage/plugin-events-node": "0.3.5",
"@internal/plugin-todo-list": "1.0.28",
"@internal/plugin-todo-list-backend": "1.0.28",
"@internal/plugin-todo-list-common": "1.0.19",
"@backstage/plugin-home": "0.7.5",
"@backstage/plugin-home-react": "0.1.14",
"@backstage/plugin-kubernetes": "0.11.11",
"@backstage/plugin-kubernetes-backend": "0.18.0",
"@backstage/plugin-kubernetes-cluster": "0.0.12",
"@backstage/plugin-kubernetes-common": "0.8.0",
"@backstage/plugin-kubernetes-node": "0.1.13",
"@backstage/plugin-kubernetes-react": "0.4.0",
"@backstage/plugin-notifications": "0.2.2",
"@backstage/plugin-notifications-backend": "0.3.0",
"@backstage/plugin-notifications-backend-module-email": "0.1.0",
"@backstage/plugin-notifications-common": "0.0.4",
"@backstage/plugin-notifications-node": "0.2.0",
"@backstage/plugin-org": "0.6.26",
"@backstage/plugin-org-react": "0.1.25",
"@backstage/plugin-permission-backend": "0.5.43",
"@backstage/plugin-permission-backend-module-allow-all-policy": "0.1.16",
"@backstage/plugin-permission-common": "0.7.14",
"@backstage/plugin-permission-node": "0.7.30",
"@backstage/plugin-permission-react": "0.4.23",
"@backstage/plugin-proxy-backend": "0.5.0",
"@backstage/plugin-scaffolder": "1.21.0",
"@backstage/plugin-scaffolder-backend": "1.22.9",
"@backstage/plugin-scaffolder-backend-module-azure": "0.1.11",
"@backstage/plugin-scaffolder-backend-module-bitbucket": "0.2.9",
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.1.9",
"@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.1.9",
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.2.20",
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.43",
"@backstage/plugin-scaffolder-backend-module-gerrit": "0.1.11",
"@backstage/plugin-scaffolder-backend-module-gitea": "0.1.9",
"@backstage/plugin-scaffolder-backend-module-github": "0.3.0",
"@backstage/plugin-scaffolder-backend-module-gitlab": "0.4.1",
"@backstage/plugin-scaffolder-backend-module-notifications": "0.0.2",
"@backstage/plugin-scaffolder-backend-module-rails": "0.4.36",
"@backstage/plugin-scaffolder-backend-module-sentry": "0.1.27",
"@backstage/plugin-scaffolder-backend-module-yeoman": "0.3.2",
"@backstage/plugin-scaffolder-common": "1.5.3",
"@backstage/plugin-scaffolder-node": "0.4.5",
"@backstage/plugin-scaffolder-node-test-utils": "0.1.5",
"@backstage/plugin-scaffolder-react": "1.9.0",
"@backstage/plugin-search": "1.4.12",
"@backstage/plugin-search-backend": "1.5.10",
"@backstage/plugin-search-backend-module-catalog": "0.1.25",
"@backstage/plugin-search-backend-module-elasticsearch": "1.5.0",
"@backstage/plugin-search-backend-module-explore": "0.1.25",
"@backstage/plugin-search-backend-module-pg": "0.5.28",
"@backstage/plugin-search-backend-module-stack-overflow-collator": "0.1.12",
"@backstage/plugin-search-backend-module-techdocs": "0.1.24",
"@backstage/plugin-search-backend-node": "1.2.24",
"@backstage/plugin-search-common": "1.2.12",
"@backstage/plugin-search-react": "1.7.12",
"@backstage/plugin-signals": "0.0.7",
"@backstage/plugin-signals-backend": "0.1.5",
"@backstage/plugin-signals-node": "0.1.5",
"@backstage/plugin-signals-react": "0.0.4",
"@backstage/plugin-techdocs": "1.10.6",
"@backstage/plugin-techdocs-addons-test-utils": "1.0.33",
"@backstage/plugin-techdocs-backend": "1.10.6",
"@backstage/plugin-techdocs-module-addons-contrib": "1.1.11",
"@backstage/plugin-techdocs-node": "1.12.5",
"@backstage/plugin-techdocs-react": "1.2.5",
"@backstage/plugin-user-settings": "0.8.7",
"@backstage/plugin-user-settings-backend": "0.2.18",
"@backstage/plugin-user-settings-common": "0.0.1"
},
"changesets": []
}
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Added mock for the Root Health Service in `mockServices`.
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-catalog-backend-module-logs': patch
---
Creates a new module to make logging catalog errors simple. This module subscribes to catalog events and logs them.
See [Backstage documentation](https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors) for details on how to install
and configure the plugin.
@@ -28,6 +28,7 @@ import { coreServices } from '@backstage/backend-plugin-api';
- [Permissions Service](./permissions.md) - Permission system integration for authorization of user actions.
- [Plugin Metadata Service](./plugin-metadata.md) - Built-in service for accessing metadata about the current plugin.
- [Root Config Service](./root-config.md) - Access to static configuration.
- [Root Health Service](./root-health.md) - Health check endpoints for the backend.
- [Root Http Router Service](./root-http-router.md) - HTTP route registration for root services.
- [Root Lifecycle Service](./root-lifecycle.md) - Registration of backend startup and shutdown lifecycle hooks.
- [Root Logger Service](./root-logger.md) - Root-level logging.
@@ -0,0 +1,40 @@
---
id: root-health
title: Root Health Service
sidebar_label: Health
description: Documentation for the Health service
---
The Root Health service provides some health check endpoints for the backend. By default, the `rootHttpRouter` exposes a `/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness` endpoints, which return a JSON object with the status of the backend services according the implementation of the Root Health Service.
## Configuring the service
The following example shows how you can override the root health service implementation.
```ts
import { RootHealthService, coreServices } from '@backstage/backend-plugin-api';
const backend = createBackend();
class MyRootHealthService implements RootHealthService {
async getLiveness() {
// provide your own implementation
return { status: 200, payload: { status: 'ok' } };
}
async getReadiness() {
// provide your own implementation
return { status: 200, payload: { status: 'ok' } };
}
}
backend.add(
createServiceFactory({
service: coreServices.rootHealth,
deps: {},
async factory({}) {
return new MyRootHealthService();
},
}),
);
```
@@ -177,3 +177,106 @@ here.
Setting this value too low risks exhausting rate limits on external systems that
are queried by processors, such as version control systems housing catalog-info
files.
## Subscribing to Catalog Errors
Catalog errors are published to the [events plugin](https://github.com/backstage/backstage/tree/master/plugins/events-node): `@backstage/plugin-events-node`. You can subscribe to events and respond to errors, for example you may wish to log them.
The first step is to add the events backend plugin to your Backstage application. Navigate to your Backstage application directory and add the plugin package.
```ts
# From your Backstage root directory
yarn --cwd packages/backend add @backstage/plugin-events-node
```
Now you can install the events backend plugin in your backend.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-events-backend/alpha'));
```
### Logging Errors
If you want to log catalog errors you can install the `@backstage/plugin-catalog-backend-module-logs` module.
Install the catalog logs module.
```ts
# From your Backstage root directory
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-logs
```
Add the module to your backend.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend-module-logs'));
```
This will log errors with a level of `warn`.
You should now see logs as the catalog emits events. Example:
```
[1] 2024-06-07T00:00:28.787Z events warn Policy check failed for user:default/guest; caused by Error: Malformed envelope, /metadata/tags must be array entity=user:default/guest location=file:/Users/foobar/code/backstage-demo-instance/examples/org.yaml
```
### Custom Error Handling
If you wish to handle catalog errors with specific logic different from logging the errors the following should help you get started. For example, you may wish to send a notification or create a ticket for someone to investigate.
Create a backend module that subscribes to the catalog error events. The topic is `experimental.catalog.errors`.
```ts title="packages/backend/src/index.ts"
import { CATALOG_ERRORS_TOPIC } from '@backstage/plugin-catalog-backend';
import {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { eventsServiceRef, EventParams } from '@backstage/plugin-events-node';
interface EventsPayload {
entity: string;
location?: string;
errors: Error[];
}
interface EventsParamsWithPayload extends EventParams {
eventPayload: EventsPayload;
}
const eventsModuleCatalogErrors = createBackendModule({
pluginId: 'events',
moduleId: 'catalog-errors',
register(env) {
env.registerInit({
deps: {
events: eventsServiceRef,
logger: coreServices.logger,
},
async init({ events, logger }) {
events.subscribe({
id: 'catalog',
topics: [CATALOG_ERRORS_TOPIC],
async onEvent(params: EventParams): Promise<void> {
const event = params as EventsParamsWithPayload;
const { entity, location, errors } = event.eventPayload;
// Add custom logic here for responding to errors
for (const error of errors) {
logger.warn(error.message, {
entity,
location,
});
}
},
});
},
});
},
});
```
Now install your module.
```ts title="packages/backend/src/index.ts"
backend.add(eventsModuleCatalogErrors);
```
@@ -197,13 +197,15 @@ cannot be parsed successfully, etc.
There are two main ways that these errors are surfaced.
First, the catalog backend will produce detailed logs that should contain
sufficient information for a reader to find the causes for errors. Since these
logs are typically not easily found by end users, this can mainly be a useful
First, the catalog backend will emit events using the [events backend plugin](https://github.com/backstage/backstage/tree/master/plugins/events-node). You can subscribe to the events. The events should contain
sufficient information for a reader to find the causes for errors. See the [configuration documentation](./configuration.md#subscribing-to-catalog-errors) for how to subscribe and log these error events.
Since these events are typically not easily found by end users, this can mainly be a useful
tool for Backstage operators who want to debug problems either with statically
registered entities that are under their control, or to help end users find
problems.
> Prior to Backstage version v1.26.0 and `@backstage/plugin-catalog-backend` v1.21.9 catalog errors were logged by default.
Second, for most classes of errors, the entity itself will contain a status
field that describes the problem. The contents of this field is shown at the top
of your entity page in Backstage, if you have placed the corresponding error
@@ -256,3 +256,51 @@ spec:
input:
url: ${{ '/root' if parameters.path !== true else parameters.path }}
```
## Use placeholders to reference remote files
#### Note: testing of this functionality is not yet supported using _create/edit_
### template.yaml
```yaml
spec:
parameters:
- $yaml: https://github.com/example/path/to/example.yaml
- title: Fill in some steps
properties:
path:
title: path
type: string
steps:
- $yaml: https://github.com//example/path/to/action.yaml
- id: fetch
name: Fetch template
action: fetch:template
input:
url: ${{ parameters.path if parameters.path else '/root' }}
```
### example.yaml
```yaml
title: Provide simple information
required:
- url
properties:
url:
title: url
type: string
```
### action.yaml
```yaml
id: publish
name: Publish files
action: publish:github
input:
repoUrl: ${{ parameters.url }}
```
+1 -1
View File
@@ -102,7 +102,7 @@ See [TechDocs Architecture](architecture.md) to get an overview of where the bel
## Get involved
Reach out to us in the **#docs-like-code** channel of our
Reach out to us in the **#techdocs** channel of our
[Discord chatroom](https://github.com/backstage/backstage#community).
## Done
@@ -0,0 +1,16 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { RootHealthService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public (undocumented)
export const rootHealthServiceFactory: () => ServiceFactory<
RootHealthService,
'root'
>;
// (No @packageDocumentation comment for this package)
```
@@ -121,6 +121,8 @@ export interface RootHttpRouterConfigureContext {
// (undocumented)
config: RootConfigService;
// (undocumented)
healthRouter: RequestHandler;
// (undocumented)
lifecycle: LifecycleService;
// (undocumented)
logger: LoggerService;
+4
View File
@@ -30,6 +30,7 @@
"./logger": "./src/entrypoints/logger/index.ts",
"./permissions": "./src/entrypoints/permissions/index.ts",
"./rootConfig": "./src/entrypoints/rootConfig/index.ts",
"./rootHealth": "./src/entrypoints/rootHealth/index.ts",
"./rootHttpRouter": "./src/entrypoints/rootHttpRouter/index.ts",
"./rootLifecycle": "./src/entrypoints/rootLifecycle/index.ts",
"./rootLogger": "./src/entrypoints/rootLogger/index.ts",
@@ -72,6 +73,9 @@
"rootConfig": [
"src/entrypoints/rootConfig/index.ts"
],
"rootHealth": [
"src/entrypoints/rootHealth/index.ts"
],
"rootHttpRouter": [
"src/entrypoints/rootHttpRouter/index.ts"
],
@@ -30,6 +30,7 @@ import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
import { loggerServiceFactory } from '@backstage/backend-defaults/logger';
import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions';
import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig';
import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth';
import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter';
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
import { rootLoggerServiceFactory } from '@backstage/backend-defaults/rootLogger';
@@ -50,6 +51,7 @@ export const defaultServiceFactories = [
lifecycleServiceFactory(),
loggerServiceFactory(),
permissionsServiceFactory(),
rootHealthServiceFactory(),
rootHttpRouterServiceFactory(),
rootLifecycleServiceFactory(),
rootLoggerServiceFactory(),
@@ -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 { rootHealthServiceFactory } from './rootHealthServiceFactory';
@@ -0,0 +1,91 @@
/*
* 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 { mockServices } from '@backstage/backend-test-utils';
import { DefaultRootHealthService } from './rootHealthServiceFactory';
describe('DefaultRootHealthService', () => {
describe('readiness', () => {
it(`should return a 500 response if the server hasn't started yet`, async () => {
const service = new DefaultRootHealthService({
lifecycle: mockServices.rootLifecycle.mock(),
});
await expect(service.getReadiness()).resolves.toEqual({
status: 503,
payload: {
message: 'Backend has not started yet',
status: 'error',
},
});
});
it('should return 200 if the server has started', async () => {
let mockServerStartedFn = () => {};
const lifecycle = mockServices.rootLifecycle.mock({
addStartupHook: jest.fn(fn => (mockServerStartedFn = fn)),
});
const service = new DefaultRootHealthService({
lifecycle,
});
mockServerStartedFn();
await expect(service.getReadiness()).resolves.toEqual({
status: 200,
payload: { status: 'ok' },
});
});
it(`should return a 500 response if the server has stopped`, async () => {
let mockServerStartedFn = () => {};
let mockServerStoppedFn = () => {};
const lifecycle = mockServices.rootLifecycle.mock({
addStartupHook: jest.fn(fn => (mockServerStartedFn = fn)),
addShutdownHook: jest.fn(fn => (mockServerStoppedFn = fn)),
});
const service = new DefaultRootHealthService({
lifecycle,
});
mockServerStartedFn();
mockServerStoppedFn();
await expect(service.getReadiness()).resolves.toEqual({
status: 503,
payload: {
message: 'Backend has not started yet',
status: 'error',
},
});
});
});
describe('liveness', () => {
it('should return 200 if the server has started', async () => {
const service = new DefaultRootHealthService({
lifecycle: mockServices.rootLifecycle.mock(),
});
await expect(service.getLiveness()).resolves.toEqual({
status: 200,
payload: { status: 'ok' },
});
});
});
});
@@ -0,0 +1,64 @@
/*
* 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 {
RootHealthService,
RootLifecycleService,
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
/** @internal */
export class DefaultRootHealthService implements RootHealthService {
#isRunning = false;
constructor(readonly options: { lifecycle: RootLifecycleService }) {
options.lifecycle.addStartupHook(() => {
this.#isRunning = true;
});
options.lifecycle.addShutdownHook(() => {
this.#isRunning = false;
});
}
async getLiveness(): Promise<{ status: number; payload?: any }> {
return { status: 200, payload: { status: 'ok' } };
}
async getReadiness(): Promise<{ status: number; payload?: any }> {
if (!this.#isRunning) {
return {
status: 503,
payload: { message: 'Backend has not started yet', status: 'error' },
};
}
return { status: 200, payload: { status: 'ok' } };
}
}
/**
* @public
*/
export const rootHealthServiceFactory = createServiceFactory({
service: coreServices.rootHealth,
deps: {
lifecycle: coreServices.rootLifecycle,
},
async factory({ lifecycle }) {
return new DefaultRootHealthService({ lifecycle });
},
});
@@ -0,0 +1,42 @@
import { RootHealthService } from '@backstage/backend-plugin-api';
/*
* 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 Router from 'express-promise-router';
import { Request, Response } from 'express';
export function createHealthRouter(options: { health: RootHealthService }) {
const router = Router();
router.get(
'.backstage/health/v1/readiness',
async (_request: Request, response: Response) => {
const { status, payload } = await options.health.getReadiness();
response.status(status).json(payload);
},
);
router.get(
'.backstage/health/v1/liveness',
async (_request: Request, response: Response) => {
const { status, payload } = await options.health.getLiveness();
response.status(status).json(payload);
},
);
return router;
}
@@ -29,6 +29,7 @@ import {
readHttpServerOptions,
} from './http';
import { DefaultRootHttpRouter } from './DefaultRootHttpRouter';
import { createHealthRouter } from './createHealthRouter';
/**
* @public
@@ -41,6 +42,7 @@ export interface RootHttpRouterConfigureContext {
config: RootConfigService;
logger: LoggerService;
lifecycle: LifecycleService;
healthRouter: RequestHandler;
applyDefaults: () => void;
}
@@ -75,8 +77,9 @@ export const rootHttpRouterServiceFactory = createServiceFactory(
config: coreServices.rootConfig,
rootLogger: coreServices.rootLogger,
lifecycle: coreServices.rootLifecycle,
health: coreServices.rootHealth,
},
async factory({ config, rootLogger, lifecycle }) {
async factory({ config, rootLogger, lifecycle, health }) {
const { indexPath, configure = defaultConfigure } = options ?? {};
const logger = rootLogger.child({ service: 'rootHttpRouter' });
const app = express();
@@ -84,6 +87,8 @@ export const rootHttpRouterServiceFactory = createServiceFactory(
const router = DefaultRootHttpRouter.create({ indexPath });
const middleware = MiddlewareFactory.create({ config, logger });
const routes = router.handler();
const healthRouter = createHealthRouter({ health });
const server = await createHttpServer(
app,
readHttpServerOptions(config.getOptionalConfig('backend')),
@@ -98,11 +103,13 @@ export const rootHttpRouterServiceFactory = createServiceFactory(
config,
logger,
lifecycle,
healthRouter,
applyDefaults() {
app.use(middleware.helmet());
app.use(middleware.cors());
app.use(middleware.compression());
app.use(middleware.logging());
app.use(healthRouter);
app.use(routes);
app.use(middleware.notFound());
app.use(middleware.error());
@@ -152,7 +152,7 @@ export class PluginTaskSchedulerImpl implements SchedulerService {
export function parseDuration(
frequency: SchedulerServiceTaskScheduleDefinition['frequency'],
): string {
if ('cron' in frequency) {
if (typeof frequency === 'object' && 'cron' in frequency) {
return frequency.cron;
}
+13
View File
@@ -194,6 +194,7 @@ export namespace coreServices {
const rootConfig: ServiceRef<RootConfigService, 'root'>;
const database: ServiceRef<DatabaseService, 'plugin'>;
const discovery: ServiceRef<DiscoveryService, 'plugin'>;
const rootHealth: ServiceRef<RootHealthService, 'root'>;
const httpAuth: ServiceRef<HttpAuthService, 'plugin'>;
const httpRouter: ServiceRef<HttpRouterService, 'plugin'>;
const lifecycle: ServiceRef<LifecycleService, 'plugin'>;
@@ -500,6 +501,18 @@ export function resolveSafeChildPath(base: string, path: string): string;
// @public
export interface RootConfigService extends Config {}
// @public (undocumented)
export interface RootHealthService {
getLiveness(): Promise<{
status: number;
payload?: JsonValue;
}>;
getReadiness(): Promise<{
status: number;
payload?: JsonValue;
}>;
}
// @public
export interface RootHttpRouterService {
use(path: string, handler: Handler): void;
@@ -0,0 +1,31 @@
/*
* 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 { JsonValue } from '@backstage/types';
/**
* @public
*/
export interface RootHealthService {
/**
* Get the liveness status of the backend.
*/
getLiveness(): Promise<{ status: number; payload?: JsonValue }>;
/**
* Get the readiness status of the backend.
*/
getReadiness(): Promise<{ status: number; payload?: JsonValue }>;
}
@@ -16,7 +16,6 @@
import { ConfigReader } from '@backstage/config';
import { HumanDuration } from '@backstage/types';
import { Duration } from 'luxon';
import { readSchedulerServiceTaskScheduleDefinitionFromConfig } from './SchedulerService';
describe('readSchedulerServiceTaskScheduleDefinitionFromConfig', () => {
@@ -35,7 +34,7 @@ describe('readSchedulerServiceTaskScheduleDefinitionFromConfig', () => {
const result = readSchedulerServiceTaskScheduleDefinitionFromConfig(config);
expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *');
expect(result.timeout).toEqual(Duration.fromISO('PT3M'));
expect(result.timeout).toEqual({ minutes: 3 });
expect((result.initialDelay as HumanDuration).minutes).toEqual(20);
expect(result.scope).toBe('global');
});
@@ -51,7 +50,7 @@ describe('readSchedulerServiceTaskScheduleDefinitionFromConfig', () => {
const result = readSchedulerServiceTaskScheduleDefinitionFromConfig(config);
expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *');
expect(result.timeout).toEqual(Duration.fromISO('PT3M'));
expect(result.timeout).toEqual({ minutes: 3 });
expect(result.initialDelay).toBeUndefined();
expect(result.scope).toBeUndefined();
});
@@ -348,14 +348,14 @@ export interface SchedulerService {
getScheduledTasks(): Promise<SchedulerServiceTaskDescriptor[]>;
}
function readDuration(config: Config, key: string): Duration | HumanDuration {
function readDuration(config: Config, key: string): HumanDuration {
if (typeof config.get(key) === 'string') {
const value = config.getString(key);
const duration = Duration.fromISO(value);
if (!duration.isValid) {
throw new Error(`Invalid duration: ${value}`);
}
return duration;
return duration.toObject();
}
return readDurationFromConfig(config, { key });
@@ -364,7 +364,7 @@ function readDuration(config: Config, key: string): Duration | HumanDuration {
function readCronOrDuration(
config: Config,
key: string,
): { cron: string } | Duration | HumanDuration {
): { cron: string } | HumanDuration {
const value = config.get(key);
if (typeof value === 'object' && (value as { cron?: string }).cron) {
return value as { cron: string };
@@ -102,6 +102,13 @@ export namespace coreServices {
import('./DiscoveryService').DiscoveryService
>({ id: 'core.discovery' });
/**
* The service reference for the plugin scoped {@link RootHealthService}.
*/
export const rootHealth = createServiceRef<
import('./RootHealthService').RootHealthService
>({ id: 'core.rootHealth', scope: 'root' });
/**
* Authentication of HTTP requests.
*
@@ -32,6 +32,7 @@ export type {
export type { RootConfigService } from './RootConfigService';
export type { DatabaseService } from './DatabaseService';
export type { DiscoveryService } from './DiscoveryService';
export type { RootHealthService } from './RootHealthService';
export type {
HttpRouterService,
HttpRouterServiceAuthPolicy,
@@ -16,7 +16,6 @@
import { ConfigReader } from '@backstage/config';
import { HumanDuration } from '@backstage/types';
import { Duration } from 'luxon';
import { readTaskScheduleDefinitionFromConfig } from './readTaskScheduleDefinitionFromConfig';
describe('readTaskScheduleDefinitionFromConfig', () => {
@@ -35,7 +34,7 @@ describe('readTaskScheduleDefinitionFromConfig', () => {
const result = readTaskScheduleDefinitionFromConfig(config);
expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *');
expect(result.timeout).toEqual(Duration.fromISO('PT3M'));
expect(result.timeout).toEqual({ minutes: 3 });
expect((result.initialDelay as HumanDuration).minutes).toEqual(20);
expect(result.scope).toBe('global');
});
@@ -51,7 +50,7 @@ describe('readTaskScheduleDefinitionFromConfig', () => {
const result = readTaskScheduleDefinitionFromConfig(config);
expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *');
expect(result.timeout).toEqual(Duration.fromISO('PT3M'));
expect(result.timeout).toEqual({ minutes: 3 });
expect(result.initialDelay).toBeUndefined();
expect(result.scope).toBeUndefined();
});
@@ -19,14 +19,14 @@ import { HumanDuration } from '@backstage/types';
import { TaskScheduleDefinition } from './types';
import { Duration } from 'luxon';
function readDuration(config: Config, key: string): Duration | HumanDuration {
function readDuration(config: Config, key: string): HumanDuration {
if (typeof config.get(key) === 'string') {
const value = config.getString(key);
const duration = Duration.fromISO(value);
if (!duration.isValid) {
throw new Error(`Invalid duration: ${value}`);
}
return duration;
return duration.toObject();
}
return readDurationFromConfig(config, { key });
+10
View File
@@ -32,6 +32,7 @@ import { LifecycleService } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PermissionsService } from '@backstage/backend-plugin-api';
import { RootConfigService } from '@backstage/backend-plugin-api';
import { RootHealthService } from '@backstage/backend-plugin-api';
import { RootHttpRouterFactoryOptions } from '@backstage/backend-defaults/rootHttpRouter';
import { RootHttpRouterService } from '@backstage/backend-plugin-api';
import { RootLifecycleService } from '@backstage/backend-plugin-api';
@@ -280,6 +281,15 @@ export namespace mockServices {
) => ServiceFactory<RootConfigService, 'root'>;
}
// (undocumented)
export namespace rootHealth {
const // (undocumented)
factory: () => ServiceFactory<RootHealthService, 'root'>;
const // (undocumented)
mock: (
partialImpl?: Partial<RootHealthService> | undefined,
) => ServiceMock<RootHealthService>;
}
// (undocumented)
export namespace rootHttpRouter {
const // (undocumented)
factory: (
@@ -24,6 +24,7 @@ import { httpRouterServiceFactory } from '@backstage/backend-defaults/httpRouter
import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
import { loggerServiceFactory } from '@backstage/backend-defaults/logger';
import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions';
import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth';
import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter';
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler';
@@ -344,6 +345,14 @@ export namespace mockServices {
}));
}
export namespace rootHealth {
export const factory = rootHealthServiceFactory;
export const mock = simpleMock(coreServices.rootHealth, () => ({
getLiveness: jest.fn(),
getReadiness: jest.fn(),
}));
}
export namespace httpRouter {
export const factory = httpRouterServiceFactory;
export const mock = simpleMock(coreServices.httpRouter, () => ({
@@ -67,7 +67,7 @@ describe('pluginCommon factory', () => {
expect(modified).toBe(true);
expectLogsToMatch(output, [
'Creating backend plugin backstage-plugin-test-common',
'Creating common plugin package backstage-plugin-test-common',
'Checking Prerequisites:',
`availability plugins${sep}test-common`,
'creating temp dir',
@@ -46,7 +46,7 @@ export const pluginCommon = createFactory<Options>({
});
Task.log();
Task.log(`Creating backend plugin ${chalk.cyan(name)}`);
Task.log(`Creating common plugin package ${chalk.cyan(name)}`);
const targetDir = ctx.isMonoRepo
? paths.resolveTargetRoot('plugins', suffix)
@@ -35,9 +35,7 @@
"@types/express": "{{versionQuery '@types/express' '4.17.6'}}",
"express": "{{versionQuery 'express' '4.17.1'}}",
"express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}",
"winston": "{{versionQuery 'winston' '3.2.1'}}",
"node-fetch": "{{versionQuery 'node-fetch' '2.6.7'}}",
"yn": "{{versionQuery 'yn' '4.0.0'}}"
"node-fetch": "{{versionQuery 'node-fetch' '2.6.7'}}"
},
"devDependencies": {
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
@@ -45,7 +43,7 @@
"@backstage/plugin-auth-backend-module-guest-provider": "{{versionQuery '@backstage/plugin-auth-backend-module-guest-provider'}}",
"@types/supertest": "{{versionQuery '@types/supertest' '2.0.12'}}",
"supertest": "{{versionQuery 'supertest' '6.2.4'}}",
"msw": "{{versionQuery 'msw' '1.0.0'}}"
"msw": "{{versionQuery 'msw' '2.3.1'}}"
},
"files": [
"dist"
@@ -17,7 +17,6 @@
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { Duration } from 'luxon';
import { catalogModuleAwsS3EntityProvider } from './catalogModuleAwsS3EntityProvider';
import { AwsS3EntityProvider } from '../providers';
@@ -62,8 +61,8 @@ describe('catalogModuleAwsS3EntityProvider', () => {
],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(usedSchedule?.frequency).toEqual({ months: 1 });
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
expect(addedProviders?.length).toEqual(1);
expect(addedProviders?.pop()?.getProviderName()).toEqual(
'awsS3-provider:default',
@@ -15,7 +15,6 @@
*/
import { ConfigReader } from '@backstage/config';
import { Duration } from 'luxon';
import { readAwsS3Configs } from './config';
describe('readAwsS3Configs', () => {
@@ -92,7 +91,7 @@ describe('readAwsS3Configs', () => {
id: 'provider4',
schedule: {
...provider4.schedule,
frequency: Duration.fromISO(provider4.schedule.frequency),
frequency: { minutes: 30 },
},
});
});
@@ -17,7 +17,6 @@
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { Duration } from 'luxon';
import { catalogModuleAzureDevOpsEntityProvider } from './catalogModuleAzureDevOpsEntityProvider';
import { AzureDevOpsEntityProvider } from '../providers';
@@ -66,8 +65,8 @@ describe('catalogModuleAzureDevOpsEntityProvider', () => {
],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(usedSchedule?.frequency).toEqual({ months: 1 });
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
expect(addedProviders?.length).toEqual(1);
expect(addedProviders?.pop()?.getProviderName()).toEqual(
'AzureDevOpsEntityProvider:test',
@@ -15,7 +15,6 @@
*/
import { ConfigReader } from '@backstage/config';
import { Duration } from 'luxon';
import { readAzureDevOpsConfigs } from './config';
describe('readAzureDevOpsConfigs', () => {
@@ -95,7 +94,7 @@ describe('readAzureDevOpsConfigs', () => {
id: 'provider4',
schedule: {
...provider4.schedule,
frequency: Duration.fromISO(provider4.schedule.frequency),
frequency: { minutes: 30 },
},
});
expect(actual[4]).toEqual({
@@ -21,7 +21,6 @@ import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
import { eventsServiceRef } from '@backstage/plugin-events-node';
import { Duration } from 'luxon';
import { catalogModuleBitbucketCloudEntityProvider } from './catalogModuleBitbucketCloudEntityProvider';
import { BitbucketCloudEntityProvider } from '../providers/BitbucketCloudEntityProvider';
@@ -78,8 +77,8 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => {
],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(usedSchedule?.frequency).toEqual({ months: 1 });
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
expect(addedProviders?.length).toEqual(1);
expect(runner).not.toHaveBeenCalled();
const provider = addedProviders!.pop()!;
@@ -15,7 +15,6 @@
*/
import { ConfigReader } from '@backstage/config';
import { Duration } from 'luxon';
import { readProviderConfigs } from './BitbucketCloudEntityProviderConfig';
describe('readProviderConfigs', () => {
@@ -130,7 +129,7 @@ describe('readProviderConfigs', () => {
repoSlug: undefined,
},
schedule: {
frequency: Duration.fromISO('PT30M'),
frequency: { minutes: 30 },
timeout: {
minutes: 3,
},
@@ -18,7 +18,6 @@ import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { catalogModuleBitbucketServerEntityProvider } from './catalogModuleBitbucketServerEntityProvider';
import { Duration } from 'luxon';
import { BitbucketServerEntityProvider } from '../providers';
describe('catalogModuleBitbucketServerEntityProvider', () => {
@@ -70,8 +69,8 @@ describe('catalogModuleBitbucketServerEntityProvider', () => {
],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(usedSchedule?.frequency).toEqual({ months: 1 });
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
expect(addedProviders?.length).toEqual(1);
expect(addedProviders?.pop()?.getProviderName()).toEqual(
'bitbucketServer-provider:default',
@@ -15,7 +15,6 @@
*/
import { ConfigReader } from '@backstage/config';
import { Duration } from 'luxon';
import { readProviderConfigs } from './BitbucketServerEntityProviderConfig';
describe('readProviderConfigs', () => {
@@ -111,7 +110,7 @@ describe('readProviderConfigs', () => {
skipArchivedRepos: undefined,
},
schedule: {
frequency: Duration.fromISO('PT30M'),
frequency: { minutes: 30 },
timeout: {
minutes: 3,
},
@@ -17,7 +17,6 @@
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { Duration } from 'luxon';
import { catalogModuleGerritEntityProvider } from './catalogModuleGerritEntityProvider';
import { GerritEntityProvider } from '../providers/GerritEntityProvider';
@@ -76,8 +75,8 @@ describe('catalogModuleGerritEntityProvider', () => {
],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(usedSchedule?.frequency).toEqual({ months: 1 });
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
expect(addedProviders?.length).toEqual(1);
expect(addedProviders?.pop()?.getProviderName()).toEqual(
'gerrit-provider:test',
@@ -15,7 +15,6 @@
*/
import { ConfigReader } from '@backstage/config';
import { Duration } from 'luxon';
import { readGerritConfigs } from './config';
describe('readGerritConfigs', () => {
@@ -63,7 +62,7 @@ describe('readGerritConfigs', () => {
id: 'active-g3',
schedule: {
...provider3.schedule,
frequency: Duration.fromISO(provider3.schedule.frequency),
frequency: { minutes: 30 },
},
});
});
@@ -18,7 +18,6 @@ import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { Duration } from 'luxon';
import { catalogModuleGithubOrgEntityProvider } from './module';
describe('catalogModuleGithubOrgEntityProvider', () => {
@@ -66,8 +65,8 @@ describe('catalogModuleGithubOrgEntityProvider', () => {
],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(usedSchedule?.frequency).toEqual({ months: 1 });
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
expect(addedProviders?.length).toEqual(1);
expect(addedProviders![0].getProviderName()).toEqual(
'GithubMultiOrgEntityProvider:default',
@@ -21,7 +21,6 @@ import {
catalogAnalysisExtensionPoint,
catalogProcessingExtensionPoint,
} from '@backstage/plugin-catalog-node/alpha';
import { Duration } from 'luxon';
import { githubCatalogModule } from './githubCatalogModule';
import { GithubLocationAnalyzer } from '../analyzers/GithubLocationAnalyzer';
@@ -75,8 +74,8 @@ describe('githubCatalogModule', () => {
],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(usedSchedule?.frequency).toEqual({ months: 1 });
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
expect(addedProviders?.length).toEqual(1);
expect(addedProviders?.pop()?.getProviderName()).toEqual(
'github-provider:default',
@@ -15,7 +15,6 @@
*/
import { ConfigReader } from '@backstage/config';
import { Duration } from 'luxon';
import { readProviderConfigs } from './GithubEntityProviderConfig';
describe('readProviderConfigs', () => {
@@ -270,7 +269,7 @@ describe('readProviderConfigs', () => {
visibility: undefined,
},
schedule: {
frequency: Duration.fromISO('PT30M'),
frequency: { minutes: 30 },
timeout: {
minutes: 3,
},
@@ -22,7 +22,6 @@ import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
import { eventsServiceRef } from '@backstage/plugin-events-node';
import { Duration } from 'luxon';
import { catalogModuleGitlabOrgDiscoveryEntityProvider } from './catalogModuleGitlabOrgDiscoveryEntityProvider';
describe('catalogModuleGitlabOrgDiscoveryEntityProvider', () => {
@@ -90,8 +89,8 @@ describe('catalogModuleGitlabOrgDiscoveryEntityProvider', () => {
],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(usedSchedule?.frequency).toEqual({ months: 1 });
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
expect(addedProviders?.length).toEqual(1);
expect(runner).not.toHaveBeenCalled();
@@ -21,7 +21,6 @@ import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
import { eventsServiceRef } from '@backstage/plugin-events-node';
import { Duration } from 'luxon';
import { GitlabDiscoveryEntityProvider } from '../providers';
import { catalogModuleGitlabDiscoveryEntityProvider } from './catalogModuleGitlabDiscoveryEntityProvider';
@@ -89,8 +88,8 @@ describe('catalogModuleGitlabDiscoveryEntityProvider', () => {
],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(usedSchedule?.frequency).toEqual({ months: 1 });
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
expect(addedProviders?.length).toEqual(1);
expect(runner).not.toHaveBeenCalled();
@@ -15,7 +15,6 @@
*/
import { ConfigReader } from '@backstage/config';
import { Duration } from 'luxon';
import { readGitlabConfigs } from './config';
describe('config', () => {
@@ -179,7 +178,7 @@ describe('config', () => {
allowInherited: false,
skipForkedRepos: false,
schedule: {
frequency: Duration.fromISO('PT30M'),
frequency: { minutes: 30 },
timeout: {
minutes: 3,
},
@@ -16,11 +16,11 @@ import { GroupTransformer as GroupTransformer_2 } from '@backstage/plugin-catalo
import { JsonValue } from '@backstage/types';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
import { SchedulerService } from '@backstage/backend-plugin-api';
import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api';
import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api';
import { SearchEntry } from 'ldapjs';
import { SearchOptions } from 'ldapjs';
import { TaskRunner } from '@backstage/backend-tasks';
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { UserEntity } from '@backstage/catalog-model';
import { UserTransformer as UserTransformer_2 } from '@backstage/plugin-catalog-backend-module-ldap';
@@ -135,7 +135,7 @@ export interface LdapOrgEntityProviderLegacyOptions {
groupTransformer?: GroupTransformer;
id: string;
logger: LoggerService;
schedule: 'manual' | TaskRunner;
schedule: 'manual' | SchedulerServiceTaskRunner;
target: string;
userTransformer?: UserTransformer;
}
@@ -145,8 +145,8 @@ export type LdapOrgEntityProviderOptions =
| LdapOrgEntityProviderLegacyOptions
| {
logger: LoggerService;
schedule?: 'manual' | TaskRunner;
scheduler?: PluginTaskScheduler;
schedule?: 'manual' | SchedulerServiceTaskRunner;
scheduler?: SchedulerService;
userTransformer?: UserTransformer | Record<string, UserTransformer>;
groupTransformer?: GroupTransformer | Record<string, GroupTransformer>;
};
@@ -199,7 +199,7 @@ export type LdapProviderConfig = {
bind?: BindConfig;
users: UserConfig;
groups: GroupConfig;
schedule?: TaskScheduleDefinition;
schedule?: SchedulerServiceTaskScheduleDefinition;
};
// @public
@@ -79,6 +79,75 @@ describe('readLdapConfig', () => {
expect(actual).toEqual(expected);
});
it('reads schedules well', () => {
const config = {
catalog: {
providers: {
ldapOrg: {
default: {
schedule: {
frequency: 'PT3M', // should work for ISO durations
timeout: { minutes: 1 },
},
target: 'target',
users: {
dn: 'udn',
},
groups: {
dn: 'gdn',
},
},
},
},
},
};
const actual = readProviderConfigs(new ConfigReader(config));
const expected = [
{
id: 'default',
target: 'target',
bind: undefined,
schedule: {
frequency: { minutes: 3 },
timeout: { minutes: 1 },
},
users: {
dn: 'udn',
options: {
scope: 'one',
attributes: ['*', '+'],
},
set: undefined,
map: {
rdn: 'uid',
name: 'uid',
displayName: 'cn',
email: 'mail',
memberOf: 'memberOf',
},
},
groups: {
dn: 'gdn',
options: {
scope: 'one',
attributes: ['*', '+'],
},
set: undefined,
map: {
rdn: 'cn',
name: 'cn',
description: 'description',
type: 'groupType',
displayName: 'cn',
memberOf: 'memberOf',
members: 'member',
},
},
},
];
expect(actual).toEqual(expected);
});
it('reads all the values', () => {
const config = {
catalog: {
@@ -15,9 +15,9 @@
*/
import {
readTaskScheduleDefinitionFromConfig,
TaskScheduleDefinition,
} from '@backstage/backend-tasks';
SchedulerServiceTaskScheduleDefinition,
readSchedulerServiceTaskScheduleDefinitionFromConfig,
} from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { SearchOptions } from 'ldapjs';
@@ -46,7 +46,7 @@ export type LdapProviderConfig = {
// The settings that govern the reading and interpretation of groups
groups: GroupConfig;
// Schedule configuration for refresh tasks.
schedule?: TaskScheduleDefinition;
schedule?: SchedulerServiceTaskScheduleDefinition;
};
/**
@@ -380,7 +380,9 @@ export function readProviderConfigs(config: Config): LdapProviderConfig[] {
const c = providersConfig.getConfig(id);
const schedule = c.has('schedule')
? readTaskScheduleDefinitionFromConfig(c.getConfig('schedule'))
? readSchedulerServiceTaskScheduleDefinitionFromConfig(
c.getConfig('schedule'),
)
: undefined;
const newConfig = {
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
import {
ANNOTATION_LOCATION,
ANNOTATION_ORIGIN_LOCATION,
@@ -35,7 +34,11 @@ import {
readLdapOrg,
UserTransformer,
} from '../ldap';
import { LoggerService } from '@backstage/backend-plugin-api';
import {
LoggerService,
SchedulerService,
SchedulerServiceTaskRunner,
} from '@backstage/backend-plugin-api';
import { readLdapLegacyConfig, readProviderConfigs } from '../ldap';
/**
@@ -60,16 +63,16 @@ export type LdapOrgEntityProviderOptions =
* manually at some interval.
*
* But more commonly you will pass in the result of
* {@link @backstage/backend-tasks#PluginTaskScheduler.createScheduledTaskRunner}
* {@link @backstage/backend-plugin-api#SchedulerService.createScheduledTaskRunner}
* to enable automatic scheduling of tasks.
*/
schedule?: 'manual' | TaskRunner;
schedule?: 'manual' | SchedulerServiceTaskRunner;
/**
* Scheduler used to schedule refreshes based on
* the schedule config.
*/
scheduler?: PluginTaskScheduler;
scheduler?: SchedulerService;
/**
* The function that transforms a user entry in msgraph to an entity.
@@ -122,10 +125,10 @@ export interface LdapOrgEntityProviderLegacyOptions {
* manually at some interval.
*
* But more commonly you will pass in the result of
* {@link @backstage/backend-tasks#PluginTaskScheduler.createScheduledTaskRunner}
* {@link @backstage/backend-plugin-api#SchedulerService.createScheduledTaskRunner}
* to enable automatic scheduling of tasks.
*/
schedule: 'manual' | TaskRunner;
schedule: 'manual' | SchedulerServiceTaskRunner;
/**
* The function that transforms a user entry in LDAP to an entity.
@@ -311,7 +314,7 @@ export class LdapOrgEntityProvider implements EntityProvider {
markCommitComplete();
}
private schedule(taskRunner: TaskRunner) {
private schedule(taskRunner: SchedulerServiceTaskRunner) {
this.scheduleFn = async () => {
const id = `${this.getProviderName()}:refresh`;
await taskRunner.run({
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,8 @@
# backstage-plugin-catalog-backend-module-logs
A module that subscribes to catalog related events and logs them.
## Getting started
See [Backstage documentation](https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors) for details on how to install
and configure the plugin.
@@ -0,0 +1,11 @@
## API Report File for "@backstage/plugin-catalog-backend-module-logs"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeatureCompat } from '@backstage/backend-plugin-api';
// @public
const catalogModuleLogs: BackendFeatureCompat;
export default catalogModuleLogs;
```
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-catalog-backend-module-logs
title: '@backstage/plugin-catalog-backend-module-logs'
description: A module that subscribes to catalog releated events and logs them.
spec:
lifecycle: experimental
type: backstage-backend-plugin-module
owner: maintainers
@@ -0,0 +1,45 @@
{
"name": "@backstage/plugin-catalog-backend-module-logs",
"version": "0.0.0",
"description": "A module that subscribes to catalog releated events and logs them.",
"backstage": {
"role": "backend-plugin-module",
"pluginId": "catalog",
"pluginPackage": "@backstage/plugin-catalog-backend"
},
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/catalog-backend-module-logs"
},
"license": "Apache-2.0",
"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",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-events-node": "workspace:^"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-events-backend-test-utils": "workspace:^"
}
}
@@ -0,0 +1,22 @@
/*
* 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.
*/
/**
* A catalog module that logs catalog errors using the logger service.
*
* @packageDocumentation
*/
export { catalogModuleLogs as default } from './module';
@@ -0,0 +1,45 @@
/*
* 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { catalogModuleLogs } from './module';
import { createServiceFactory } from '@backstage/backend-plugin-api';
import { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
import { eventsServiceRef } from '@backstage/plugin-events-node';
describe('catalogModuleLogs', () => {
it('should be correctly wired and set up', async () => {
const events = new TestEventsService();
const eventsServiceFactory = createServiceFactory({
service: eventsServiceRef,
deps: {},
async factory({}) {
return events;
},
});
await startTestBackend({
features: [
mockServices.logger.factory(),
eventsServiceFactory(),
catalogModuleLogs(),
],
});
expect(events.subscribed).toHaveLength(1);
expect(events.subscribed[0].id).toEqual('catalog');
});
});
@@ -0,0 +1,67 @@
/*
* 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 {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { CATALOG_ERRORS_TOPIC } from '@backstage/plugin-catalog-backend';
import { eventsServiceRef, EventParams } from '@backstage/plugin-events-node';
interface EventsPayload {
entity: string;
location?: string;
errors: Error[];
}
interface EventsParamsWithPayload extends EventParams {
eventPayload: EventsPayload;
}
/**
* A catalog module that logs catalog errors using the logger service.
*
* @packageDocumentation
* @public
*/
export const catalogModuleLogs = createBackendModule({
pluginId: 'catalog',
moduleId: 'logs',
register(env) {
env.registerInit({
deps: {
events: eventsServiceRef,
logger: coreServices.logger,
},
async init({ events, logger }) {
events.subscribe({
id: 'catalog',
topics: [CATALOG_ERRORS_TOPIC],
async onEvent(params: EventParams): Promise<void> {
const event = params as EventsParamsWithPayload;
const { entity, location, errors } = event.eventPayload;
for (const error of errors) {
logger.warn(error.message, {
entity,
location,
});
}
},
});
},
});
},
});
@@ -15,7 +15,6 @@
*/
import { ConfigReader } from '@backstage/config';
import { Duration } from 'luxon';
import { readMicrosoftGraphConfig, readProviderConfigs } from './config';
describe('readMicrosoftGraphConfig', () => {
@@ -204,7 +203,7 @@ describe('readProviderConfigs', () => {
groupSelect: ['id', 'displayName', 'description'],
groupFilter: 'securityEnabled eq false',
schedule: {
frequency: Duration.fromISO('PT30M'),
frequency: { minutes: 30 },
timeout: {
minutes: 3,
},
@@ -17,7 +17,6 @@
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { Duration } from 'luxon';
import { catalogModuleMicrosoftGraphOrgEntityProvider } from './catalogModuleMicrosoftGraphOrgEntityProvider';
import { MicrosoftGraphOrgEntityProvider } from '../processors';
@@ -67,8 +66,8 @@ describe('catalogModuleMicrosoftGraphOrgEntityProvider', () => {
],
});
expect(usedSchedule?.frequency).toEqual(Duration.fromISO('PT30M'));
expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M'));
expect(usedSchedule?.frequency).toEqual({ minutes: 30 });
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
expect(addedProviders?.length).toEqual(1);
expect(addedProviders?.pop()?.getProviderName()).toEqual(
'MicrosoftGraphOrgEntityProvider:customProviderId',
@@ -16,7 +16,6 @@
import { ConfigReader } from '@backstage/config';
import { readProviderConfigs } from './PuppetDbEntityProviderConfig';
import { Duration } from 'luxon';
describe('readProviderConfigs', () => {
afterEach(() => jest.resetAllMocks());
@@ -120,10 +119,8 @@ describe('readProviderConfigs', () => {
expect(providerConfigs).toHaveLength(1);
expect(providerConfigs[0].schedule).toEqual({
frequency: Duration.fromISO('PT30M'),
timeout: {
minutes: 10,
},
frequency: { minutes: 30 },
timeout: { minutes: 10 },
});
});
});
+6
View File
@@ -23,7 +23,9 @@ export const EntityMembersListCard: (props: {
memberDisplayTitle?: string | undefined;
pageSize?: number | undefined;
showAggregateMembersToggle?: boolean | undefined;
relationType?: string | undefined;
relationsType?: EntityRelationAggregation | undefined;
relationAggregation?: EntityRelationAggregation | undefined;
}) => JSX_2.Element;
// @public (undocumented)
@@ -32,6 +34,7 @@ export const EntityOwnershipCard: (props: {
entityFilterKind?: string[] | undefined;
hideRelationsToggle?: boolean | undefined;
relationsType?: EntityRelationAggregation | undefined;
relationAggregation?: EntityRelationAggregation | undefined;
entityLimit?: number | undefined;
}) => JSX_2.Element;
@@ -55,7 +58,9 @@ export const MembersListCard: (props: {
memberDisplayTitle?: string;
pageSize?: number;
showAggregateMembersToggle?: boolean;
relationType?: string;
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
}) => React_2.JSX.Element;
// @public
@@ -82,6 +87,7 @@ export const OwnershipCard: (props: {
entityFilterKind?: string[];
hideRelationsToggle?: boolean;
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
entityLimit?: number;
}) => React_2.JSX.Element;
@@ -99,6 +99,7 @@ describe('MemberTab Test', () => {
] as Entity[],
}),
};
const getEntitiesSpy = jest.spyOn(catalogApi, 'getEntities');
it('Display Profile Card', async () => {
await renderInTestApp(
@@ -115,6 +116,12 @@ describe('MemberTab Test', () => {
},
},
);
expect(getEntitiesSpy).toHaveBeenCalledWith({
filter: {
kind: 'User',
'relations.memberof': ['group:default/team-d'],
},
});
expect(screen.getByAltText('Tara MacGovern')).toHaveAttribute(
'src',
@@ -149,6 +156,29 @@ describe('MemberTab Test', () => {
expect(screen.getByText('Testers (1)')).toBeInTheDocument();
});
it('Can query a different relationship', async () => {
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<EntityProvider entity={groupEntity}>
<MembersListCard relationType="leaderOf" />
</EntityProvider>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
'/catalog': rootRouteRef,
},
},
);
expect(getEntitiesSpy).toHaveBeenCalledWith({
filter: {
kind: 'User',
'relations.leaderof': ['group:default/team-d'],
},
});
});
describe('Aggregate members toggle', () => {
it('Does not show the aggregate members toggle if the showAggregateMembersToggle prop is undefined', async () => {
await renderInTestApp(
@@ -346,7 +376,7 @@ describe('MemberTab Test', () => {
<EntityLayout.Route path="/" title="Title">
<MembersListCard
showAggregateMembersToggle
relationsType="aggregated"
relationAggregation="aggregated"
/>
</EntityLayout.Route>
</EntityLayout>
@@ -384,7 +414,7 @@ describe('MemberTab Test', () => {
<EntityProvider entity={groupA}>
<EntityLayout>
<EntityLayout.Route path="/" title="Title">
<MembersListCard relationsType="aggregated" />
<MembersListCard relationAggregation="aggregated" />
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
@@ -138,14 +138,19 @@ export const MembersListCard = (props: {
memberDisplayTitle?: string;
pageSize?: number;
showAggregateMembersToggle?: boolean;
relationType?: string;
/** @deprecated Please use `relationAggregation` instead */
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
}) => {
const {
memberDisplayTitle = 'Members',
pageSize = 50,
showAggregateMembersToggle,
relationsType = 'direct',
relationType = 'memberof',
} = props;
const relationAggregation =
props.relationAggregation ?? props.relationsType ?? 'direct';
const classes = useListStyles();
const { entity: groupEntity } = useEntity<GroupEntity>();
@@ -165,7 +170,7 @@ export const MembersListCard = (props: {
};
const [showAggregateMembers, setShowAggregateMembers] = useState(
relationsType === 'aggregated',
relationAggregation === 'aggregated',
);
const { loading: loadingDescendantMembers, value: descendantMembers } =
@@ -177,6 +182,7 @@ export const MembersListCard = (props: {
return await getAllDesendantMembersForGroupEntity(
groupEntity,
catalogApi,
relationType,
);
}, [catalogApi, groupEntity, showAggregateMembers]);
const {
@@ -187,7 +193,7 @@ export const MembersListCard = (props: {
const membersList = await catalogApi.getEntities({
filter: {
kind: 'User',
'relations.memberof': [
[`relations.${relationType.toLocaleLowerCase('en-US')}`]: [
stringifyEntityRef({
kind: 'group',
namespace: groupNamespace.toLocaleLowerCase('en-US'),
@@ -114,19 +114,27 @@ export const ComponentsGrid = ({
className,
entity,
relationsType,
relationAggregation,
entityFilterKind,
entityLimit = 6,
}: {
className?: string;
entity: Entity;
relationsType: EntityRelationAggregation;
/** @deprecated Please use relationAggregation instead */
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
entityFilterKind?: string[];
entityLimit?: number;
}) => {
const catalogLink = useRouteRef(catalogIndexRouteRef);
if (!relationsType && !relationAggregation) {
throw new Error(
'The relationAggregation property must be set as an EntityRelationAggregation type.',
);
}
const { componentsWithCounters, loading, error } = useGetEntities(
entity,
relationsType,
(relationAggregation ?? relationsType)!, // we can safely use the non-null assertion here because of the run-time check above
entityFilterKind,
entityLimit,
);
@@ -288,7 +288,7 @@ describe('OwnershipCard', () => {
const { getByText } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<EntityProvider entity={userEntity}>
<OwnershipCard relationsType="aggregated" />
<OwnershipCard relationAggregation="aggregated" />
</EntityProvider>
</TestApiProvider>,
{
@@ -67,31 +67,34 @@ export const OwnershipCard = (props: {
variant?: InfoCardVariants;
entityFilterKind?: string[];
hideRelationsToggle?: boolean;
/** @deprecated Please use relationAggregation instead */
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
entityLimit?: number;
}) => {
const {
variant,
entityFilterKind,
hideRelationsToggle,
relationsType,
entityLimit = 6,
} = props;
const relationAggregation = props.relationAggregation ?? props.relationsType;
const relationsToggle =
hideRelationsToggle === undefined ? false : hideRelationsToggle;
const classes = useStyles();
const { entity } = useEntity();
const defaultRelationsType = entity.kind === 'User' ? 'aggregated' : 'direct';
const [getRelationsType, setRelationsType] = useState(
relationsType ?? defaultRelationsType,
const defaultRelationAggregation =
entity.kind === 'User' ? 'aggregated' : 'direct';
const [getRelationAggregation, setRelationAggregation] = useState(
relationAggregation ?? defaultRelationAggregation,
);
useEffect(() => {
if (!relationsType) {
setRelationsType(defaultRelationsType);
if (!relationAggregation) {
setRelationAggregation(defaultRelationAggregation);
}
}, [setRelationsType, defaultRelationsType, relationsType]);
}, [setRelationAggregation, defaultRelationAggregation, relationAggregation]);
return (
<InfoCard
@@ -112,16 +115,18 @@ export const OwnershipCard = (props: {
placement="top"
arrow
title={`${
getRelationsType === 'direct' ? 'Direct' : 'Aggregated'
getRelationAggregation === 'direct' ? 'Direct' : 'Aggregated'
} Relations`}
>
<Switch
color="primary"
checked={getRelationsType !== 'direct'}
checked={getRelationAggregation !== 'direct'}
onChange={() => {
const updatedRelationsType =
getRelationsType === 'direct' ? 'aggregated' : 'direct';
setRelationsType(updatedRelationsType);
const updatedRelationAggregation =
getRelationAggregation === 'direct'
? 'aggregated'
: 'direct';
setRelationAggregation(updatedRelationAggregation);
}}
name="pin"
inputProps={{ 'aria-label': 'Ownership Type Switch' }}
@@ -136,7 +141,7 @@ export const OwnershipCard = (props: {
className={classes.grid}
entity={entity}
entityLimit={entityLimit}
relationsType={getRelationsType}
relationAggregation={getRelationAggregation}
entityFilterKind={entityFilterKind}
/>
</InfoCard>
@@ -64,7 +64,7 @@ describe('useGetEntities', () => {
]),
});
describe('given aggregated relationsType', () => {
describe('given aggregated relationAggregation', () => {
const whenHookIsCalledWith = async (_entity: Entity) => {
const { result } = renderHook(
({ entity }) => useGetEntities(entity, 'aggregated'),
@@ -205,7 +205,7 @@ describe('useGetEntities', () => {
});
});
describe('given direct relationsType', () => {
describe('given direct relationAggregation', () => {
const whenHookIsCalledWith = async (_entity: Entity) => {
const { result } = renderHook(
({ entity }) => useGetEntities(entity, 'direct'),
@@ -125,11 +125,11 @@ const getChildOwnershipEntityRefs = async (
const getOwners = async (
entity: Entity,
relations: EntityRelationAggregation,
relationAggregation: EntityRelationAggregation,
catalogApi: CatalogApi,
): Promise<string[]> => {
const isGroup = entity.kind === 'Group';
const isAggregated = relations === 'aggregated';
const isAggregated = relationAggregation === 'aggregated';
const isUserEntity = entity.kind === 'User';
if (isAggregated && isGroup) {
@@ -166,7 +166,7 @@ const getOwnedEntitiesByOwners = (
export function useGetEntities(
entity: Entity,
relations: EntityRelationAggregation,
relationAggregation: EntityRelationAggregation,
entityFilterKind?: string[],
entityLimit = 6,
): {
@@ -189,7 +189,7 @@ export function useGetEntities(
error,
value: componentsWithCounters,
} = useAsync(async () => {
const owners = await getOwners(entity, relations, catalogApi);
const owners = await getOwners(entity, relationAggregation, catalogApi);
const ownedEntitiesList = await getOwnedEntitiesByOwners(
owners,
@@ -230,7 +230,7 @@ export function useGetEntities(
kind: string;
queryParams: string;
}>;
}, [catalogApi, entity, relations]);
}, [catalogApi, entity, relationAggregation]);
return {
componentsWithCounters,
+11 -7
View File
@@ -31,6 +31,7 @@ import {
export const getMembersFromGroups = async (
groups: CompoundEntityRef[],
catalogApi: CatalogApi,
relationship = 'memberof',
) => {
const membersList =
groups.length === 0
@@ -38,13 +39,14 @@ export const getMembersFromGroups = async (
: await catalogApi.getEntities({
filter: {
kind: 'User',
'relations.memberof': groups.map(group =>
stringifyEntityRef({
kind: 'group',
namespace: group.namespace.toLocaleLowerCase('en-US'),
name: group.name.toLocaleLowerCase('en-US'),
}),
),
[`relations.${relationship.toLocaleLowerCase('en-US')}`]:
groups.map(group =>
stringifyEntityRef({
kind: 'group',
namespace: group.namespace.toLocaleLowerCase('en-US'),
name: group.name.toLocaleLowerCase('en-US'),
}),
),
},
});
@@ -99,10 +101,12 @@ export const getDescendantGroupsFromGroup = async (
export const getAllDesendantMembersForGroupEntity = async (
groupEntity: GroupEntity,
catalogApi: CatalogApi,
relationship = 'memberof',
) =>
getMembersFromGroups(
await getDescendantGroupsFromGroup(groupEntity, catalogApi),
catalogApi,
relationship,
);
export const removeDuplicateEntitiesFrom = (entityArray: Entity[]) => {
+13
View File
@@ -5440,6 +5440,19 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-catalog-backend-module-logs@workspace:plugins/catalog-backend-module-logs":
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog-backend-module-logs@workspace:plugins/catalog-backend-module-logs"
dependencies:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/plugin-catalog-backend": "workspace:^"
"@backstage/plugin-events-backend-test-utils": "workspace:^"
"@backstage/plugin-events-node": "workspace:^"
languageName: unknown
linkType: soft
"@backstage/plugin-catalog-backend-module-msgraph@workspace:plugins/catalog-backend-module-msgraph":
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog-backend-module-msgraph@workspace:plugins/catalog-backend-module-msgraph"