diff --git a/.changeset/bright-panthers-leave.md b/.changeset/bright-panthers-leave.md new file mode 100644 index 0000000000..35d9c0f869 --- /dev/null +++ b/.changeset/bright-panthers-leave.md @@ -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. diff --git a/.changeset/curvy-teachers-smell.md b/.changeset/curvy-teachers-smell.md new file mode 100644 index 0000000000..3f032f2177 --- /dev/null +++ b/.changeset/curvy-teachers-smell.md @@ -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 diff --git a/.changeset/friendly-stingrays-occur.md b/.changeset/friendly-stingrays-occur.md new file mode 100644 index 0000000000..3383203b40 --- /dev/null +++ b/.changeset/friendly-stingrays-occur.md @@ -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; diff --git a/.changeset/funny-laws-tease.md b/.changeset/funny-laws-tease.md new file mode 100644 index 0000000000..8ecbc87b65 --- /dev/null +++ b/.changeset/funny-laws-tease.md @@ -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...`. diff --git a/.changeset/little-games-fail.md b/.changeset/little-games-fail.md new file mode 100644 index 0000000000..199f983be8 --- /dev/null +++ b/.changeset/little-games-fail.md @@ -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. diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..dcb909ba99 --- /dev/null +++ b/.changeset/pre.json @@ -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": [] +} diff --git a/.changeset/serious-kings-trade.md b/.changeset/serious-kings-trade.md new file mode 100644 index 0000000000..826392bf98 --- /dev/null +++ b/.changeset/serious-kings-trade.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Added mock for the Root Health Service in `mockServices`. diff --git a/.changeset/ten-pots-walk.md b/.changeset/ten-pots-walk.md new file mode 100644 index 0000000000..cf540fd0e2 --- /dev/null +++ b/.changeset/ten-pots-walk.md @@ -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. diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 04d2e57ae8..3be12b2e26 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -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. diff --git a/docs/backend-system/core-services/root-health.md b/docs/backend-system/core-services/root-health.md new file mode 100644 index 0000000000..fc73ba9546 --- /dev/null +++ b/docs/backend-system/core-services/root-health.md @@ -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(); + }, + }), +); +``` diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 61366a91f7..3bbbfd6716 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -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 { + 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); +``` diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md index 13fe496fc1..1489688720 100644 --- a/docs/features/software-catalog/life-of-an-entity.md +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -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 diff --git a/docs/features/software-templates/input-examples.md b/docs/features/software-templates/input-examples.md index 50495b1784..5ab43fdca3 100644 --- a/docs/features/software-templates/input-examples.md +++ b/docs/features/software-templates/input-examples.md @@ -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 }} +``` diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index a696b850a8..beec6d1fad 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -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 diff --git a/packages/backend-defaults/api-report-rootHealth.md b/packages/backend-defaults/api-report-rootHealth.md new file mode 100644 index 0000000000..ffa067605a --- /dev/null +++ b/packages/backend-defaults/api-report-rootHealth.md @@ -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) +``` diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md index b753406202..d9727fd36d 100644 --- a/packages/backend-defaults/api-report-rootHttpRouter.md +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -121,6 +121,8 @@ export interface RootHttpRouterConfigureContext { // (undocumented) config: RootConfigService; // (undocumented) + healthRouter: RequestHandler; + // (undocumented) lifecycle: LifecycleService; // (undocumented) logger: LoggerService; diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 4c34c7f317..2d84bae308 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -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" ], diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 9b3fa5d649..e3195748a6 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.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(), diff --git a/packages/backend-defaults/src/entrypoints/rootHealth/index.ts b/packages/backend-defaults/src/entrypoints/rootHealth/index.ts new file mode 100644 index 0000000000..35225b39c4 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHealth/index.ts @@ -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'; diff --git a/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.test.ts new file mode 100644 index 0000000000..0eee5c18c6 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.test.ts @@ -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' }, + }); + }); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.ts new file mode 100644 index 0000000000..c8b4ad5394 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.ts @@ -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 }); + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts new file mode 100644 index 0000000000..13691ede28 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts @@ -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; +} diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts index ea3dc42eb7..22f5c2f310 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -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()); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts index 62b36e024c..40aa9d521c 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts @@ -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; } diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 258cdefe1b..e9762f652e 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -194,6 +194,7 @@ export namespace coreServices { const rootConfig: ServiceRef; const database: ServiceRef; const discovery: ServiceRef; + const rootHealth: ServiceRef; const httpAuth: ServiceRef; const httpRouter: ServiceRef; const lifecycle: ServiceRef; @@ -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; diff --git a/packages/backend-plugin-api/src/services/definitions/RootHealthService.ts b/packages/backend-plugin-api/src/services/definitions/RootHealthService.ts new file mode 100644 index 0000000000..e6c56e8654 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/RootHealthService.ts @@ -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 }>; +} diff --git a/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts b/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts index 7b20487af4..875e24786f 100644 --- a/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts +++ b/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts @@ -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(); }); diff --git a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts index 91edf27ad6..a7ec79a336 100644 --- a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts @@ -348,14 +348,14 @@ export interface SchedulerService { getScheduledTasks(): Promise; } -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 }; diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index b0518fb9d4..33d1da2903 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -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. * diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 6f30c5f980..5e30d444fd 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -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, diff --git a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts index c52d59b016..adeb134611 100644 --- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts +++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts @@ -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(); }); diff --git a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts index 5a173f246b..e94d06d5c3 100644 --- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts +++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts @@ -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 }); diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 019d398ea6..a2fb4b7674 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -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; } // (undocumented) + export namespace rootHealth { + const // (undocumented) + factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; + } + // (undocumented) export namespace rootHttpRouter { const // (undocumented) factory: ( diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 24571b634c..d56179d958 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -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, () => ({ diff --git a/packages/cli/src/lib/new/factories/pluginCommon.test.ts b/packages/cli/src/lib/new/factories/pluginCommon.test.ts index d85e490510..82c1c87169 100644 --- a/packages/cli/src/lib/new/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/new/factories/pluginCommon.test.ts @@ -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', diff --git a/packages/cli/src/lib/new/factories/pluginCommon.ts b/packages/cli/src/lib/new/factories/pluginCommon.ts index 560386c618..5f694124ea 100644 --- a/packages/cli/src/lib/new/factories/pluginCommon.ts +++ b/packages/cli/src/lib/new/factories/pluginCommon.ts @@ -46,7 +46,7 @@ export const pluginCommon = createFactory({ }); 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) diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 6b7aa915aa..c20a1854a2 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -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" diff --git a/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts b/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts index 4addd1cb45..9165eb4322 100644 --- a/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts +++ b/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts @@ -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', diff --git a/plugins/catalog-backend-module-aws/src/providers/config.test.ts b/plugins/catalog-backend-module-aws/src/providers/config.test.ts index fb2f3c0790..4c7ec9a964 100644 --- a/plugins/catalog-backend-module-aws/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-aws/src/providers/config.test.ts @@ -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 }, }, }); }); diff --git a/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts b/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts index 2c674a8061..e406c61e07 100644 --- a/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts +++ b/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts @@ -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', diff --git a/plugins/catalog-backend-module-azure/src/providers/config.test.ts b/plugins/catalog-backend-module-azure/src/providers/config.test.ts index 02dbaab90e..5d5ae930c1 100644 --- a/plugins/catalog-backend-module-azure/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-azure/src/providers/config.test.ts @@ -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({ diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts index db53b0b41b..bcf13e786d 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts @@ -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()!; diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts index 5a2cc99437..7f13616586 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts @@ -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, }, diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts index 56b44b29f5..5335b04e2d 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts @@ -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', diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.test.ts index 9e8aca5dd9..755fa4b58a 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.test.ts @@ -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, }, diff --git a/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts b/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts index 66f0cbab2c..dc40e0525d 100644 --- a/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts @@ -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', diff --git a/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts b/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts index ecf68d9f3c..85ab3383f0 100644 --- a/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts @@ -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 }, }, }); }); diff --git a/plugins/catalog-backend-module-github-org/src/module.test.ts b/plugins/catalog-backend-module-github-org/src/module.test.ts index 6e03d8d9cc..76ab4d1fbc 100644 --- a/plugins/catalog-backend-module-github-org/src/module.test.ts +++ b/plugins/catalog-backend-module-github-org/src/module.test.ts @@ -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', diff --git a/plugins/catalog-backend-module-github/src/module/githubCatalogModule.test.ts b/plugins/catalog-backend-module-github/src/module/githubCatalogModule.test.ts index 59f8b41a6d..018d339de8 100644 --- a/plugins/catalog-backend-module-github/src/module/githubCatalogModule.test.ts +++ b/plugins/catalog-backend-module-github/src/module/githubCatalogModule.test.ts @@ -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', diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts index c83bfe5db1..296cbc12df 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts @@ -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, }, diff --git a/plugins/catalog-backend-module-gitlab-org/src/catalogModuleGitlabOrgDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab-org/src/catalogModuleGitlabOrgDiscoveryEntityProvider.test.ts index 1078936950..5fbc321cc2 100644 --- a/plugins/catalog-backend-module-gitlab-org/src/catalogModuleGitlabOrgDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab-org/src/catalogModuleGitlabOrgDiscoveryEntityProvider.test.ts @@ -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(); diff --git a/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts index e4cff56bbd..636405220b 100644 --- a/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts @@ -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(); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts index 09b42e6475..a5c3ab2b28 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -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, }, diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index 84de581387..de245b8cd6 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -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; groupTransformer?: GroupTransformer | Record; }; @@ -199,7 +199,7 @@ export type LdapProviderConfig = { bind?: BindConfig; users: UserConfig; groups: GroupConfig; - schedule?: TaskScheduleDefinition; + schedule?: SchedulerServiceTaskScheduleDefinition; }; // @public diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts index ddea9de03f..4edfaeebea 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts @@ -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: { diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.ts index 2bd21fd166..7c4795fc46 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -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 = { diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts index fb8f27d685..0e5fae55bb 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts @@ -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({ diff --git a/plugins/catalog-backend-module-logs/.eslintrc.js b/plugins/catalog-backend-module-logs/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/catalog-backend-module-logs/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-logs/README.md b/plugins/catalog-backend-module-logs/README.md new file mode 100644 index 0000000000..1771dbe02c --- /dev/null +++ b/plugins/catalog-backend-module-logs/README.md @@ -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. diff --git a/plugins/catalog-backend-module-logs/api-report.md b/plugins/catalog-backend-module-logs/api-report.md new file mode 100644 index 0000000000..0be2197c9a --- /dev/null +++ b/plugins/catalog-backend-module-logs/api-report.md @@ -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; +``` diff --git a/plugins/catalog-backend-module-logs/catalog-info.yaml b/plugins/catalog-backend-module-logs/catalog-info.yaml new file mode 100644 index 0000000000..f2223174c3 --- /dev/null +++ b/plugins/catalog-backend-module-logs/catalog-info.yaml @@ -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 diff --git a/plugins/catalog-backend-module-logs/package.json b/plugins/catalog-backend-module-logs/package.json new file mode 100644 index 0000000000..858f7ec198 --- /dev/null +++ b/plugins/catalog-backend-module-logs/package.json @@ -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:^" + } +} diff --git a/plugins/catalog-backend-module-logs/src/index.ts b/plugins/catalog-backend-module-logs/src/index.ts new file mode 100644 index 0000000000..77a7d424eb --- /dev/null +++ b/plugins/catalog-backend-module-logs/src/index.ts @@ -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'; diff --git a/plugins/catalog-backend-module-logs/src/module.test.ts b/plugins/catalog-backend-module-logs/src/module.test.ts new file mode 100644 index 0000000000..79f1b8c295 --- /dev/null +++ b/plugins/catalog-backend-module-logs/src/module.test.ts @@ -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'); + }); +}); diff --git a/plugins/catalog-backend-module-logs/src/module.ts b/plugins/catalog-backend-module-logs/src/module.ts new file mode 100644 index 0000000000..bdc916e958 --- /dev/null +++ b/plugins/catalog-backend-module-logs/src/module.ts @@ -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 { + const event = params as EventsParamsWithPayload; + const { entity, location, errors } = event.eventPayload; + for (const error of errors) { + logger.warn(error.message, { + entity, + location, + }); + } + }, + }); + }, + }); + }, +}); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index b02894dfd9..c0e9f61f29 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -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, }, diff --git a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts index 5742fbbc6c..2a3e66a9b3 100644 --- a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts @@ -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', diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProviderConfig.test.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProviderConfig.test.ts index 8da0fb4150..86335cb97d 100644 --- a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProviderConfig.test.ts @@ -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 }, }); }); }); diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md index a6926d71f2..e958e8e29a 100644 --- a/plugins/org/api-report.md +++ b/plugins/org/api-report.md @@ -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; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 793ffe274b..69d6090687 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -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( + + + + + , + { + 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', () => { @@ -384,7 +414,7 @@ describe('MemberTab Test', () => { - + diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index 418a83f5b2..f27b071114 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -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(); @@ -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'), diff --git a/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx b/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx index 69ad5bf8bb..a7decfb592 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx @@ -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, ); diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index f2b949f768..004fb8b964 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -288,7 +288,7 @@ describe('OwnershipCard', () => { const { getByText } = await renderInTestApp( - + , { diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index a5b0b7bb19..fce47845bc 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -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 ( { - 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} /> diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts index eac5a171ba..c6cec557c1 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts @@ -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'), diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts index 5a708bcfee..d806bbfef2 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts @@ -125,11 +125,11 @@ const getChildOwnershipEntityRefs = async ( const getOwners = async ( entity: Entity, - relations: EntityRelationAggregation, + relationAggregation: EntityRelationAggregation, catalogApi: CatalogApi, ): Promise => { 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, diff --git a/plugins/org/src/helpers/helpers.ts b/plugins/org/src/helpers/helpers.ts index 7da4ee32d8..465f89afa1 100644 --- a/plugins/org/src/helpers/helpers.ts +++ b/plugins/org/src/helpers/helpers.ts @@ -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[]) => { diff --git a/yarn.lock b/yarn.lock index 7f0bd34e43..af72fdba43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"