diff --git a/.changeset/breezy-cycles-count.md b/.changeset/breezy-cycles-count.md new file mode 100644 index 0000000000..81998d9e8b --- /dev/null +++ b/.changeset/breezy-cycles-count.md @@ -0,0 +1,39 @@ +--- +'@backstage/plugin-events-backend-test-utils': patch +'@backstage/plugin-events-backend': patch +'@backstage/plugin-events-node': patch +--- + +Add new `EventsService` as well as `eventsServiceRef` for the new backend system. + +**Summary:** + +- new: + `EventsService`, `eventsServiceRef`, `TestEventsService` +- deprecated: + `EventBroker`, `EventPublisher`, `EventSubscriber`, `DefaultEventBroker`, `EventsBackend`, + most parts of `EventsExtensionPoint` (alpha), + `TestEventBroker`, `TestEventPublisher`, `TestEventSubscriber` + +Add the `eventsServiceRef` as dependency to your backend plugins +or backend plugin modules. + +**Details:** + +The previous implementation using the `EventsExtensionPoint` was added in the early stages +of the new backend system and does not respect the plugin isolation. +This made it not compatible anymore with the new backend system. + +Additionally, the previous interfaces had some room for simplification, +supporting less exposure of internal concerns as well. + +Hereby, this change adds a new `EventsService` interface as replacement for the now deprecated `EventBroker`. +The new interface does not require any `EventPublisher` or `EventSubscriber` interfaces anymore. +Instead, it is expected that the `EventsService` gets passed into publishers and subscribers, +and used internally. There is no need to expose anything of that at their own interfaces. + +Most parts of `EventsExtensionPoint` (alpha) are deprecated as well and were not usable +(by other plugins or their modules) anyway. + +The `DefaultEventBroker` implementation is deprecated and wraps the new `DefaultEventsService` implementation. +Optionally, an instance can be passed as argument to allow mixed setups to operate alongside. diff --git a/.changeset/clever-eagles-boil.md b/.changeset/clever-eagles-boil.md new file mode 100644 index 0000000000..a2f47ed092 --- /dev/null +++ b/.changeset/clever-eagles-boil.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-adr': patch +'@backstage/plugin-adr-common': patch +--- + +Fixed Azure DevOps ADR file path reading diff --git a/.changeset/cold-boats-sell.md b/.changeset/cold-boats-sell.md new file mode 100644 index 0000000000..5b0eba7301 --- /dev/null +++ b/.changeset/cold-boats-sell.md @@ -0,0 +1,23 @@ +--- +'@backstage/plugin-auth-backend-module-guest-provider': minor +--- + +Adds a new guest provider that maps guest users to actual tokens. This also shifts the default guest login to `user:development/guest` to reduce overlap with your production/real data. To change that (or set it back to the old default, use the new `auth.providers.guest.userEntityRef` config key) like so, + +```yaml title=app-config.yaml +auth: + providers: + guest: + userEntityRef: user:default/guest +``` + +This also adds a new property to control the ownership entity refs, + +```yaml title=app-config.yaml +auth: + providers: + guest: + ownershipEntityRefs: + - guests + - development/custom +``` diff --git a/.changeset/cyan-dryers-share.md b/.changeset/cyan-dryers-share.md new file mode 100644 index 0000000000..4c1ce1d553 --- /dev/null +++ b/.changeset/cyan-dryers-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/errors': patch +--- + +Fixed an issue that was causing ResponseError not to report the HTTP status from the provided response. diff --git a/.changeset/eight-fireants-crash.md b/.changeset/eight-fireants-crash.md new file mode 100644 index 0000000000..a4eb903585 --- /dev/null +++ b/.changeset/eight-fireants-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-playlist-backend': patch +--- + +Migrated to support new auth services. diff --git a/.changeset/empty-wolves-rule.md b/.changeset/empty-wolves-rule.md new file mode 100644 index 0000000000..a549cf3628 --- /dev/null +++ b/.changeset/empty-wolves-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Fix jacoco convertor to not require annotation to be set to scm-only. diff --git a/.changeset/fifty-insects-yell.md b/.changeset/fifty-insects-yell.md new file mode 100644 index 0000000000..feedd844a5 --- /dev/null +++ b/.changeset/fifty-insects-yell.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Added `mockServices.userInfo`, which now also automatically is made available in test backends. diff --git a/.changeset/fifty-moons-study.md b/.changeset/fifty-moons-study.md new file mode 100644 index 0000000000..3744960870 --- /dev/null +++ b/.changeset/fifty-moons-study.md @@ -0,0 +1,5 @@ +--- +'@backstage/theme': patch +--- + +Exported `defaultTypography` to make adjusting these values in a custom theme easier diff --git a/.changeset/five-beers-accept.md b/.changeset/five-beers-accept.md new file mode 100644 index 0000000000..d5dbcf88d7 --- /dev/null +++ b/.changeset/five-beers-accept.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Removed hardcoded font-family on select input diff --git a/.changeset/five-hats-accept.md b/.changeset/five-hats-accept.md new file mode 100644 index 0000000000..11ee4ff276 --- /dev/null +++ b/.changeset/five-hats-accept.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications': patch +--- + +The Notifications can be newly filtered based on the Created Date. diff --git a/.changeset/five-mayflies-juggle.md b/.changeset/five-mayflies-juggle.md new file mode 100644 index 0000000000..56bf28a8e9 --- /dev/null +++ b/.changeset/five-mayflies-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-sites-backend': patch +--- + +Added new backend system for the Azure Sites backend plugin diff --git a/.changeset/forty-oranges-joke.md b/.changeset/forty-oranges-joke.md new file mode 100644 index 0000000000..28d162442c --- /dev/null +++ b/.changeset/forty-oranges-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration-react': patch +--- + +Updated `microsoftAuthApi` scopes for Azure DevOps to be fully qualified. diff --git a/.changeset/fresh-rings-tell.md b/.changeset/fresh-rings-tell.md new file mode 100644 index 0000000000..82fe952e4a --- /dev/null +++ b/.changeset/fresh-rings-tell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Fix handling of default plugins that have configuration diff --git a/.changeset/friendly-coats-travel.md b/.changeset/friendly-coats-travel.md new file mode 100644 index 0000000000..70d684a3da --- /dev/null +++ b/.changeset/friendly-coats-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Migrated to support new auth services. diff --git a/.changeset/friendly-news-sin.md b/.changeset/friendly-news-sin.md new file mode 100644 index 0000000000..af64190f9c --- /dev/null +++ b/.changeset/friendly-news-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Create a component abstraction to consume system icons. diff --git a/.changeset/healthy-experts-rhyme.md b/.changeset/healthy-experts-rhyme.md new file mode 100644 index 0000000000..ca4618f1f5 --- /dev/null +++ b/.changeset/healthy-experts-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-sites-backend': minor +--- + +**BREAKING**: The `createRouter` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. diff --git a/.changeset/heavy-coats-sniff.md b/.changeset/heavy-coats-sniff.md new file mode 100644 index 0000000000..c503ec0dec --- /dev/null +++ b/.changeset/heavy-coats-sniff.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-jenkins-backend': minor +--- + +**BREAKING**: Both `createRouter` and `DefaultJenkinsInfoProvider.fromConfig` now require the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. + +The `JenkinsInfoProvider` interface has been updated to receive `credentials` of the type `BackstageCredentials` rather than a token. diff --git a/.changeset/hungry-points-burn.md b/.changeset/hungry-points-burn.md new file mode 100644 index 0000000000..c0dabf0446 --- /dev/null +++ b/.changeset/hungry-points-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Added 'root' and 'label' class keys for EntityAutocompletePicker, EntityOwnerPicker and EntityProcessingStatusPicker diff --git a/.changeset/kind-pants-speak.md b/.changeset/kind-pants-speak.md new file mode 100644 index 0000000000..215cd5fadc --- /dev/null +++ b/.changeset/kind-pants-speak.md @@ -0,0 +1,21 @@ +--- +'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket': patch +'@backstage/plugin-scaffolder-backend-module-gerrit': patch +'@backstage/plugin-scaffolder-backend-module-github': patch +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/plugin-scaffolder-backend-module-sentry': patch +'@backstage/plugin-scaffolder-backend-module-yeoman': patch +'@backstage/plugin-scaffolder-backend-module-azure': patch +'@backstage/plugin-scaffolder-backend-module-gitea': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +'@backstage/plugin-scaffolder-node-test-utils': minor +'@backstage/plugin-scaffolder-backend': patch +--- + +Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + +It will help to maintain tests in a long run during structural changes of action context. diff --git a/.changeset/kind-students-cross.md b/.changeset/kind-students-cross.md new file mode 100644 index 0000000000..9f9d2d40a6 --- /dev/null +++ b/.changeset/kind-students-cross.md @@ -0,0 +1,76 @@ +--- +'@backstage/plugin-events-backend-module-bitbucket-cloud': minor +'@backstage/plugin-events-backend-module-gerrit': minor +'@backstage/plugin-events-backend-module-github': minor +'@backstage/plugin-events-backend-module-gitlab': minor +'@backstage/plugin-events-backend-module-azure': minor +'@backstage/plugin-events-node': minor +--- + +BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + +`EventRouter` uses the new `EventsService` instead of the `EventBroker` now, +causing a breaking change to its signature. + +All of its extensions and implementations got adjusted accordingly. +(`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, +`GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + +Required adjustments were made to all backend modules for the new backend system, +now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + +**Migration:** + +Example for implementations of `SubTopicEventRouter`: + +```diff + import { + EventParams, ++ EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { +- constructor() { +- super('github'); ++ constructor(options: { events: EventsService }) { ++ super({ ++ events: options.events, ++ topic: 'github', ++ }); + } + ++ protected getSubscriberId(): string { ++ return 'GithubEventRouter'; ++ } ++ + // ... + } +``` + +Example for a direct extension of `EventRouter`: + +```diff + class MyEventRouter extends EventRouter { +- constructor(/* ... */) { ++ constructor(options: { ++ events: EventsService; ++ // ... ++ }) { +- super(); + // ... ++ super({ ++ events: options.events, ++ topics: topics, ++ }); + } ++ ++ protected getSubscriberId(): string { ++ return 'MyEventRouter'; ++ } +- +- supportsEventTopics(): string[] { +- return this.topics; +- } + } +``` diff --git a/.changeset/lazy-terms-shake.md b/.changeset/lazy-terms-shake.md new file mode 100644 index 0000000000..efe89f1c12 --- /dev/null +++ b/.changeset/lazy-terms-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-linguist': patch +--- + +Get component's title from translation file. See: https://backstage.io/docs/plugins/internationalization#for-an-application-developer-overwrite-plugin-messages diff --git a/.changeset/lemon-lemons-sparkle.md b/.changeset/lemon-lemons-sparkle.md new file mode 100644 index 0000000000..6aae1a7f8d --- /dev/null +++ b/.changeset/lemon-lemons-sparkle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +The entity page extension provided by the `/alpha` plugin now correctly renders the entity 404 page. diff --git a/.changeset/long-emus-talk.md b/.changeset/long-emus-talk.md new file mode 100644 index 0000000000..ac8d5cbc68 --- /dev/null +++ b/.changeset/long-emus-talk.md @@ -0,0 +1,32 @@ +--- +'@backstage/plugin-events-backend-module-aws-sqs': minor +--- + +BREAKING CHANGE: Migrate `AwsSqsConsumingEventPublisher` and its backend module to use `EventsService`. + +Uses the `EventsService` instead of `EventBroker` at `AwsSqsConsumingEventPublisher`, +dropping the use of `EventPublisher` including `setEventBroker(..)`. + +Now, `AwsSqsConsumingEventPublisher.fromConfig` requires `events: EventsService` as option. + +```diff + const sqs = AwsSqsConsumingEventPublisher.fromConfig({ + config: env.config, ++ events: env.events, + logger: env.logger, + scheduler: env.scheduler, + }); ++ await Promise.all(sqs.map(publisher => publisher.start())); + + // e.g. at packages/backend/src/plugins/events.ts +- await new EventsBackend(env.logger) +- .setEventBroker(env.eventBroker) +- .addPublishers(sqs) +- .start(); + + // or for other kinds of setups +- await Promise.all(sqs.map(publisher => publisher.setEventBroker(eventBroker))); +``` + +`eventsModuleAwsSqsConsumingEventPublisher` uses the `eventsServiceRef` as dependency, +instead of `eventsExtensionPoint`. diff --git a/.changeset/loud-dolls-exist.md b/.changeset/loud-dolls-exist.md new file mode 100644 index 0000000000..5dce8d71f2 --- /dev/null +++ b/.changeset/loud-dolls-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Added the `UserInfoApi` as both an optional input and as an output for `createLegacyAuthAdapters` diff --git a/.changeset/modern-impalas-add.md b/.changeset/modern-impalas-add.md new file mode 100644 index 0000000000..8964e07fea --- /dev/null +++ b/.changeset/modern-impalas-add.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Removed the inline color from select icon to allow it to be colored via a theme diff --git a/.changeset/nice-beans-wait.md b/.changeset/nice-beans-wait.md new file mode 100644 index 0000000000..7cbdbc0fdf --- /dev/null +++ b/.changeset/nice-beans-wait.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-search-backend-module-techdocs': patch +'@backstage/plugin-search-backend-module-catalog': patch +'@backstage/plugin-search-backend-module-explore': patch +--- + +Migrated to support new auth services. diff --git a/.changeset/polite-zoos-pay.md b/.changeset/polite-zoos-pay.md new file mode 100644 index 0000000000..9596d1356c --- /dev/null +++ b/.changeset/polite-zoos-pay.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend': minor +'@backstage/plugin-catalog-node': minor +--- + +Allow setting EntityDataParser using CatalogModelExtensionPoint diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..8a921bf194 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,280 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "example-app": "0.2.92", + "@backstage/app-defaults": "1.5.0", + "example-app-next": "0.0.6", + "app-next-example-plugin": "0.0.6", + "example-backend": "0.2.92", + "@backstage/backend-app-api": "0.5.11", + "@backstage/backend-common": "0.21.0", + "@backstage/backend-defaults": "0.2.10", + "@backstage/backend-dev-utils": "0.1.4", + "@backstage/backend-dynamic-feature-service": "0.2.0", + "example-backend-next": "0.0.20", + "@backstage/backend-openapi-utils": "0.1.3", + "@backstage/backend-plugin-api": "0.6.10", + "@backstage/backend-tasks": "0.5.15", + "@backstage/backend-test-utils": "0.3.0", + "@backstage/catalog-client": "1.6.0", + "@backstage/catalog-model": "1.4.4", + "@backstage/cli": "0.25.2", + "@backstage/cli-common": "0.1.13", + "@backstage/cli-node": "0.2.3", + "@backstage/codemods": "0.1.47", + "@backstage/config": "1.1.1", + "@backstage/config-loader": "1.6.2", + "@backstage/core-app-api": "1.12.0", + "@backstage/core-compat-api": "0.2.0", + "@backstage/core-components": "0.14.0", + "@backstage/core-plugin-api": "1.9.0", + "@backstage/create-app": "0.5.11", + "@backstage/dev-utils": "1.0.27", + "e2e-test": "0.2.12", + "@backstage/e2e-test-utils": "0.1.1", + "@backstage/errors": "1.2.3", + "@backstage/eslint-plugin": "0.1.5", + "@backstage/frontend-app-api": "0.6.0", + "@backstage/frontend-plugin-api": "0.6.0", + "@backstage/frontend-test-utils": "0.1.2", + "@backstage/integration": "1.9.0", + "@backstage/integration-aws-node": "0.1.9", + "@backstage/integration-react": "1.1.24", + "@backstage/release-manifests": "0.0.11", + "@backstage/repo-tools": "0.6.0", + "@techdocs/cli": "1.8.2", + "techdocs-cli-embedded-app": "0.2.91", + "@backstage/test-utils": "1.5.0", + "@backstage/theme": "0.5.1", + "@backstage/types": "1.1.1", + "@backstage/version-bridge": "1.0.7", + "@backstage/plugin-adr": "0.6.13", + "@backstage/plugin-adr-backend": "0.4.7", + "@backstage/plugin-adr-common": "0.2.20", + "@backstage/plugin-airbrake": "0.3.30", + "@backstage/plugin-airbrake-backend": "0.3.7", + "@backstage/plugin-allure": "0.1.46", + "@backstage/plugin-analytics-module-ga": "0.2.0", + "@backstage/plugin-analytics-module-ga4": "0.2.0", + "@backstage/plugin-analytics-module-newrelic-browser": "0.1.0", + "@backstage/plugin-apache-airflow": "0.2.20", + "@backstage/plugin-api-docs": "0.11.0", + "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.6", + "@backstage/plugin-apollo-explorer": "0.1.20", + "@backstage/plugin-app-backend": "0.3.58", + "@backstage/plugin-app-node": "0.1.10", + "@backstage/plugin-app-visualizer": "0.1.1", + "@backstage/plugin-auth-backend": "0.21.0", + "@backstage/plugin-auth-backend-module-atlassian-provider": "0.1.2", + "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.1.0", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.2.4", + "@backstage/plugin-auth-backend-module-github-provider": "0.1.7", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.1.7", + "@backstage/plugin-auth-backend-module-google-provider": "0.1.7", + "@backstage/plugin-auth-backend-module-microsoft-provider": "0.1.5", + "@backstage/plugin-auth-backend-module-oauth2-provider": "0.1.7", + "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.1.2", + "@backstage/plugin-auth-backend-module-oidc-provider": "0.1.0", + "@backstage/plugin-auth-backend-module-okta-provider": "0.0.3", + "@backstage/plugin-auth-backend-module-pinniped-provider": "0.1.4", + "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.1.2", + "@backstage/plugin-auth-node": "0.4.4", + "@backstage/plugin-azure-devops": "0.3.12", + "@backstage/plugin-azure-devops-backend": "0.5.2", + "@backstage/plugin-azure-devops-common": "0.3.2", + "@backstage/plugin-azure-sites": "0.1.19", + "@backstage/plugin-azure-sites-backend": "0.2.0", + "@backstage/plugin-azure-sites-common": "0.1.2", + "@backstage/plugin-badges": "0.2.54", + "@backstage/plugin-badges-backend": "0.3.7", + "@backstage/plugin-bazaar": "0.2.22", + "@backstage/plugin-bazaar-backend": "0.3.8", + "@backstage/plugin-bitbucket-cloud-common": "0.2.16", + "@backstage/plugin-bitrise": "0.1.57", + "@backstage/plugin-catalog": "1.17.0", + "@backstage/plugin-catalog-backend": "1.17.0", + "@backstage/plugin-catalog-backend-module-aws": "0.3.4", + "@backstage/plugin-catalog-backend-module-azure": "0.1.29", + "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.1.3", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.25", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.23", + "@backstage/plugin-catalog-backend-module-gcp": "0.1.10", + "@backstage/plugin-catalog-backend-module-gerrit": "0.1.26", + "@backstage/plugin-catalog-backend-module-github": "0.5.0", + "@backstage/plugin-catalog-backend-module-github-org": "0.1.4", + "@backstage/plugin-catalog-backend-module-gitlab": "0.3.7", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.4.14", + "@backstage/plugin-catalog-backend-module-ldap": "0.5.25", + "@backstage/plugin-catalog-backend-module-msgraph": "0.5.17", + "@backstage/plugin-catalog-backend-module-openapi": "0.1.27", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.1.15", + "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.1.7", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.3.7", + "@backstage/plugin-catalog-common": "1.0.21", + "@backstage/plugin-catalog-graph": "0.4.0", + "@backstage/plugin-catalog-import": "0.10.6", + "@backstage/plugin-catalog-node": "1.7.0", + "@backstage/plugin-catalog-react": "1.10.0", + "@backstage/plugin-catalog-unprocessed-entities": "0.1.8", + "@backstage/plugin-cicd-statistics": "0.1.32", + "@backstage/plugin-cicd-statistics-module-gitlab": "0.1.26", + "@backstage/plugin-circleci": "0.3.30", + "@backstage/plugin-cloudbuild": "0.4.0", + "@backstage/plugin-code-climate": "0.1.30", + "@backstage/plugin-code-coverage": "0.2.23", + "@backstage/plugin-code-coverage-backend": "0.2.24", + "@backstage/plugin-codescene": "0.1.22", + "@backstage/plugin-config-schema": "0.1.50", + "@backstage/plugin-cost-insights": "0.12.19", + "@backstage/plugin-cost-insights-common": "0.1.2", + "@backstage/plugin-devtools": "0.1.9", + "@backstage/plugin-devtools-backend": "0.2.7", + "@backstage/plugin-devtools-common": "0.1.8", + "@backstage/plugin-dynatrace": "9.0.0", + "@backstage/plugin-entity-feedback": "0.2.13", + "@backstage/plugin-entity-feedback-backend": "0.2.7", + "@backstage/plugin-entity-feedback-common": "0.1.3", + "@backstage/plugin-entity-validation": "0.1.15", + "@backstage/plugin-events-backend": "0.2.19", + "@backstage/plugin-events-backend-module-aws-sqs": "0.2.13", + "@backstage/plugin-events-backend-module-azure": "0.1.20", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.1.20", + "@backstage/plugin-events-backend-module-gerrit": "0.1.20", + "@backstage/plugin-events-backend-module-github": "0.1.20", + "@backstage/plugin-events-backend-module-gitlab": "0.1.20", + "@backstage/plugin-events-backend-test-utils": "0.1.20", + "@backstage/plugin-events-node": "0.2.19", + "@internal/plugin-todo-list": "1.0.22", + "@internal/plugin-todo-list-backend": "1.0.22", + "@internal/plugin-todo-list-common": "1.0.17", + "@backstage/plugin-explore": "0.4.16", + "@backstage/plugin-explore-backend": "0.0.20", + "@backstage/plugin-explore-common": "0.0.2", + "@backstage/plugin-explore-react": "0.0.36", + "@backstage/plugin-firehydrant": "0.2.14", + "@backstage/plugin-fossa": "0.2.62", + "@backstage/plugin-gcalendar": "0.3.23", + "@backstage/plugin-gcp-projects": "0.3.46", + "@backstage/plugin-git-release-manager": "0.3.42", + "@backstage/plugin-github-actions": "0.6.11", + "@backstage/plugin-github-deployments": "0.1.61", + "@backstage/plugin-github-issues": "0.2.19", + "@backstage/plugin-github-pull-requests-board": "0.1.24", + "@backstage/plugin-gitops-profiles": "0.3.45", + "@backstage/plugin-gocd": "0.1.36", + "@backstage/plugin-graphiql": "0.3.3", + "@backstage/plugin-graphql-voyager": "0.1.12", + "@backstage/plugin-home": "0.6.2", + "@backstage/plugin-home-react": "0.1.8", + "@backstage/plugin-ilert": "0.2.19", + "@backstage/plugin-jenkins": "0.9.5", + "@backstage/plugin-jenkins-backend": "0.3.4", + "@backstage/plugin-jenkins-common": "0.1.24", + "@backstage/plugin-kafka": "0.3.30", + "@backstage/plugin-kafka-backend": "0.3.8", + "@backstage/plugin-kubernetes": "0.11.5", + "@backstage/plugin-kubernetes-backend": "0.15.0", + "@backstage/plugin-kubernetes-cluster": "0.0.6", + "@backstage/plugin-kubernetes-common": "0.7.4", + "@backstage/plugin-kubernetes-node": "0.1.4", + "@backstage/plugin-kubernetes-react": "0.3.0", + "@backstage/plugin-lighthouse": "0.4.15", + "@backstage/plugin-lighthouse-backend": "0.4.2", + "@backstage/plugin-lighthouse-common": "0.1.4", + "@backstage/plugin-linguist": "0.1.15", + "@backstage/plugin-linguist-backend": "0.5.7", + "@backstage/plugin-linguist-common": "0.1.2", + "@backstage/plugin-microsoft-calendar": "0.1.12", + "@backstage/plugin-newrelic": "0.3.45", + "@backstage/plugin-newrelic-dashboard": "0.3.5", + "@backstage/plugin-nomad": "0.1.11", + "@backstage/plugin-nomad-backend": "0.1.12", + "@backstage/plugin-notifications": "0.0.1", + "@backstage/plugin-notifications-backend": "0.0.1", + "@backstage/plugin-notifications-common": "0.0.1", + "@backstage/plugin-notifications-node": "0.0.1", + "@backstage/plugin-octopus-deploy": "0.2.12", + "@backstage/plugin-opencost": "0.2.5", + "@backstage/plugin-org": "0.6.20", + "@backstage/plugin-org-react": "0.1.19", + "@backstage/plugin-pagerduty": "0.7.2", + "@backstage/plugin-periskop": "0.1.28", + "@backstage/plugin-periskop-backend": "0.2.8", + "@backstage/plugin-permission-backend": "0.5.33", + "@backstage/plugin-permission-backend-module-allow-all-policy": "0.1.7", + "@backstage/plugin-permission-common": "0.7.12", + "@backstage/plugin-permission-node": "0.7.21", + "@backstage/plugin-permission-react": "0.4.20", + "@backstage/plugin-playlist": "0.2.4", + "@backstage/plugin-playlist-backend": "0.3.14", + "@backstage/plugin-playlist-common": "0.1.14", + "@backstage/plugin-proxy-backend": "0.4.8", + "@backstage/plugin-puppetdb": "0.1.13", + "@backstage/plugin-rollbar": "0.4.30", + "@backstage/plugin-rollbar-backend": "0.1.55", + "@backstage/plugin-scaffolder": "1.18.0", + "@backstage/plugin-scaffolder-backend": "1.21.0", + "@backstage/plugin-scaffolder-backend-module-azure": "0.1.2", + "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.2.0", + "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.1.0", + "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.1.0", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.2.11", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.34", + "@backstage/plugin-scaffolder-backend-module-gerrit": "0.1.2", + "@backstage/plugin-scaffolder-backend-module-gitea": "0.1.0", + "@backstage/plugin-scaffolder-backend-module-github": "0.2.0", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-rails": "0.4.27", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.18", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.31", + "@backstage/plugin-scaffolder-common": "1.5.0", + "@backstage/plugin-scaffolder-node": "0.3.0", + "@backstage/plugin-scaffolder-react": "1.8.0", + "@backstage/plugin-search": "1.4.6", + "@backstage/plugin-search-backend": "1.5.0", + "@backstage/plugin-search-backend-module-catalog": "0.1.14", + "@backstage/plugin-search-backend-module-elasticsearch": "1.3.13", + "@backstage/plugin-search-backend-module-explore": "0.1.14", + "@backstage/plugin-search-backend-module-pg": "0.5.19", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.1.3", + "@backstage/plugin-search-backend-module-techdocs": "0.1.14", + "@backstage/plugin-search-backend-node": "1.2.14", + "@backstage/plugin-search-common": "1.2.10", + "@backstage/plugin-search-react": "1.7.6", + "@backstage/plugin-sentry": "0.5.15", + "@backstage/plugin-shortcuts": "0.3.19", + "@backstage/plugin-signals": "0.0.1", + "@backstage/plugin-signals-backend": "0.0.1", + "@backstage/plugin-signals-node": "0.0.1", + "@backstage/plugin-signals-react": "0.0.1", + "@backstage/plugin-sonarqube": "0.7.12", + "@backstage/plugin-sonarqube-backend": "0.2.12", + "@backstage/plugin-sonarqube-react": "0.1.13", + "@backstage/plugin-splunk-on-call": "0.4.19", + "@backstage/plugin-stack-overflow": "0.1.25", + "@backstage/plugin-stack-overflow-backend": "0.2.14", + "@backstage/plugin-stackstorm": "0.1.11", + "@backstage/plugin-tech-insights": "0.3.22", + "@backstage/plugin-tech-insights-backend": "0.5.24", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.42", + "@backstage/plugin-tech-insights-common": "0.2.12", + "@backstage/plugin-tech-insights-node": "0.4.16", + "@backstage/plugin-tech-radar": "0.6.13", + "@backstage/plugin-techdocs": "1.10.0", + "@backstage/plugin-techdocs-addons-test-utils": "1.0.27", + "@backstage/plugin-techdocs-backend": "1.9.3", + "@backstage/plugin-techdocs-module-addons-contrib": "1.1.5", + "@backstage/plugin-techdocs-node": "1.11.2", + "@backstage/plugin-techdocs-react": "1.1.16", + "@backstage/plugin-todo": "0.2.34", + "@backstage/plugin-todo-backend": "0.3.8", + "@backstage/plugin-user-settings": "0.8.1", + "@backstage/plugin-user-settings-backend": "0.2.9", + "@backstage/plugin-vault": "0.1.25", + "@backstage/plugin-vault-backend": "0.4.3", + "@backstage/plugin-vault-node": "0.1.3", + "@backstage/plugin-xcmetrics": "0.2.48" + }, + "changesets": [] +} diff --git a/.changeset/pretty-boats-promise.md b/.changeset/pretty-boats-promise.md new file mode 100644 index 0000000000..751ccaf14c --- /dev/null +++ b/.changeset/pretty-boats-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Added 'root' and 'label' class key to TemplateCategoryPicker diff --git a/.changeset/purple-kiwis-complain.md b/.changeset/purple-kiwis-complain.md new file mode 100644 index 0000000000..a5bcb3ed67 --- /dev/null +++ b/.changeset/purple-kiwis-complain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Migrated to support new auth services. The `CatalogBuilder.create` method now accepts a `discovery` option, which is recommended to forward from the plugin environment, as it will otherwise fall back to use the `HostDiscovery` implementation. diff --git a/.changeset/rare-dryers-check.md b/.changeset/rare-dryers-check.md new file mode 100644 index 0000000000..584481f54d --- /dev/null +++ b/.changeset/rare-dryers-check.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +--- + +**BREAKING**: The `KubernetesBuilder.createBuilder` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. diff --git a/.changeset/red-taxis-swim.md b/.changeset/red-taxis-swim.md new file mode 100644 index 0000000000..13c758d552 --- /dev/null +++ b/.changeset/red-taxis-swim.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Use the `AppIcon` component in the navigation item extension. diff --git a/.changeset/renovate-08c5b50.md b/.changeset/renovate-08c5b50.md new file mode 100644 index 0000000000..9ccefc8f0e --- /dev/null +++ b/.changeset/renovate-08c5b50.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Updated dependency `testcontainers` to `^10.0.0`. diff --git a/.changeset/renovate-6a81dd3.md b/.changeset/renovate-6a81dd3.md new file mode 100644 index 0000000000..9cf6fee161 --- /dev/null +++ b/.changeset/renovate-6a81dd3.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-adr-backend': patch +--- + +Updated dependency `marked` to `^12.0.0`. diff --git a/.changeset/renovate-7aa519f.md b/.changeset/renovate-7aa519f.md new file mode 100644 index 0000000000..763d94217b --- /dev/null +++ b/.changeset/renovate-7aa519f.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-proxy-backend': patch +--- + +Updated dependency `yup` to `^1.0.0`. diff --git a/.changeset/renovate-9850908.md b/.changeset/renovate-9850908.md new file mode 100644 index 0000000000..4911d08027 --- /dev/null +++ b/.changeset/renovate-9850908.md @@ -0,0 +1,38 @@ +--- +'@backstage/backend-common': patch +'@backstage/backend-tasks': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-auth-node': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-catalog-backend-module-ldap': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-backend-module-puppetdb': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-linguist-backend': patch +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications-node': patch +'@backstage/plugin-permission-common': patch +'@backstage/plugin-playlist-backend': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-search-backend-module-elasticsearch': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-signals-backend': patch +'@backstage/plugin-signals-node': patch +'@backstage/plugin-signals': patch +'@backstage/plugin-tech-insights-backend': patch +--- + +Updated dependency `uuid` to `^9.0.0`. +Updated dependency `@types/uuid` to `^9.0.0`. diff --git a/.changeset/renovate-ea48bac.md b/.changeset/renovate-ea48bac.md new file mode 100644 index 0000000000..ca56d7490a --- /dev/null +++ b/.changeset/renovate-ea48bac.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Updated dependency `yauzl` to `^3.0.0`. diff --git a/.changeset/selfish-glasses-cheer.md b/.changeset/selfish-glasses-cheer.md new file mode 100644 index 0000000000..be267eb0f3 --- /dev/null +++ b/.changeset/selfish-glasses-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +`SignInPage`'s `'guest'` provider now supports the `@backstage/plugin-auth-backend-module-guest-provider` package to generate tokens. It will continue to use the old frontend-only auth as a fallback. diff --git a/.changeset/silver-flowers-trade.md b/.changeset/silver-flowers-trade.md new file mode 100644 index 0000000000..8701de375c --- /dev/null +++ b/.changeset/silver-flowers-trade.md @@ -0,0 +1,40 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': minor +--- + +BREAKING CHANGE: Migrates the `BitbucketCloudEntityProvider` to use the `EventsService`; fix new backend system support. + +`BitbucketCloudEntityProvider.fromConfig` accepts `events: EventsService` as optional argument to its `options`. +With provided `events`, the event-based updates/refresh will be available. +However, the `EventSubscriber` interface was removed including its `supportsEventTopics()` and `onEvent(params)`. + +The event subscription happens on `connect(connection)` if the `events` is available. + +**Migration:** + +```diff + const bitbucketCloudProvider = BitbucketCloudEntityProvider.fromConfig( + env.config, + { + catalogApi: new CatalogClient({ discoveryApi: env.discovery }), ++ events: env.events, + logger: env.logger, + scheduler: env.scheduler, + tokenManager: env.tokenManager, + }, + ); +- env.eventBroker.subscribe(bitbucketCloudProvider); +``` + +**New Backend System:** + +Before this change, using this module with the new backend system was broken. +Now, you can add the catalog module for Bitbucket Cloud incl. event support backend. +Event support will always be enabled. +However, no updates/refresh will happen without receiving events. + +```ts +backend.add( + import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'), +); +``` diff --git a/.changeset/silver-impalas-run.md b/.changeset/silver-impalas-run.md new file mode 100644 index 0000000000..22df521680 --- /dev/null +++ b/.changeset/silver-impalas-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Fixed an issue generating a wrong entity link at the end of the import process diff --git a/.changeset/six-nails-hammer.md b/.changeset/six-nails-hammer.md new file mode 100644 index 0000000000..d0cf7a0ea7 --- /dev/null +++ b/.changeset/six-nails-hammer.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dynamic-feature-service': patch +--- + +Add `events: EventsService` to `LegacyPluginEnvironment`. diff --git a/.changeset/soft-otters-report.md b/.changeset/soft-otters-report.md new file mode 100644 index 0000000000..d71d3a2e9a --- /dev/null +++ b/.changeset/soft-otters-report.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +Add support for Service Tokens to Cloudflare Access auth provider diff --git a/.changeset/sour-olives-carry.md b/.changeset/sour-olives-carry.md new file mode 100644 index 0000000000..d0c3bdd6d0 --- /dev/null +++ b/.changeset/sour-olives-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Made the `DefaultUserInfoService` claims check stricter diff --git a/.changeset/tender-carrots-care.md b/.changeset/tender-carrots-care.md new file mode 100644 index 0000000000..8afdbcf884 --- /dev/null +++ b/.changeset/tender-carrots-care.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-notifications-backend': minor +'@backstage/plugin-notifications': minor +'@backstage/plugin-notifications-common': patch +--- + +The Notifications frontend has been redesigned towards list view with condensed row details. The 'done' attribute has been removed to keep the Notifications aligned with the idea of a messaging system instead of a task manager. diff --git a/.changeset/unlucky-jobs-report.md b/.changeset/unlucky-jobs-report.md new file mode 100644 index 0000000000..4eab12d53e --- /dev/null +++ b/.changeset/unlucky-jobs-report.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-permission-backend': patch +--- + +Migrated to use the new auth services introduced in [BEP-0003](https://github.com/backstage/backstage/blob/master/beps/0003-auth-architecture-evolution/README.md). + +The `createRouter` function now accepts `auth`, `httpAuth` and `userInfo` options. Theses are used internally to support the new backend system, and can be ignored. diff --git a/.changeset/wet-sheep-reply.md b/.changeset/wet-sheep-reply.md new file mode 100644 index 0000000000..27635b133e --- /dev/null +++ b/.changeset/wet-sheep-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Allow the `spec.target` field to be searchable in the catalog table for locations. Previously, only the `spec.targets` field was be searchable. This makes locations generated by providers such as the `GithubEntityProvider` searchable in the catalog table. [#23098](https://github.com/backstage/backstage/issues/23098) diff --git a/.changeset/young-flies-wash.md b/.changeset/young-flies-wash.md new file mode 100644 index 0000000000..dcedc9bb7c --- /dev/null +++ b/.changeset/young-flies-wash.md @@ -0,0 +1,35 @@ +--- +'@backstage/plugin-events-backend': minor +--- + +BREAKING CHANGE: Migrate `HttpPostIngressEventPublisher` and `eventsPlugin` to use `EventsService`. + +Uses the `EventsService` instead of `EventBroker` at `HttpPostIngressEventPublisher`, +dropping the use of `EventPublisher` including `setEventBroker(..)`. + +Now, `HttpPostIngressEventPublisher.fromConfig` requires `events: EventsService` as option. + +```diff + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, ++ events: env.events, + logger: env.logger, + }); + http.bind(eventsRouter); + + // e.g. at packages/backend/src/plugins/events.ts +- await new EventsBackend(env.logger) +- .setEventBroker(env.eventBroker) +- .addPublishers(http) +- .start(); + + // or for other kinds of setups +- await Promise.all(http.map(publisher => publisher.setEventBroker(eventBroker))); +``` + +`eventsPlugin` uses the `eventsServiceRef` as dependency. +Unsupported (and deprecated) extension point methods will throw an error to prevent unintended behavior. + +```ts +import { eventsServiceRef } from '@backstage/plugin-events-node'; +``` diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 70fb16ac48..e12aba2e0e 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -51,7 +51,7 @@ jobs: - run: yarn build-storybook - - uses: chromaui/action@c9067691aca4a28d6fbb40d9eea6e144369fbcae # v10 + - uses: chromaui/action@fd0e276c344bab4dc69a023fdf89ffb9b79b3b31 # v11 with: token: ${{ secrets.GITHUB_TOKEN }} # projectToken intentionally shared to allow collaborators to run Chromatic on forks diff --git a/OWNERS.md b/OWNERS.md index 4c47f28253..610d8bf984 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -73,17 +73,14 @@ Team: @backstage/permission-maintainers Scope: The Permission Framework and plugins integrating with the permission framework -| Name | Organization | Team | GitHub | Discord | -| -------------------- | ------------ | --------------- | ----------------------------------------------- | ---------------- | -| Ainhoa Larumbe | Spotify | Imaginary Goats | [ainhoaL](http://github.com/ainhoaL) | ainhoa#8085 | -| Claire Casey | Spotify | Imaginary Goats | [clairelcasey](http://github.com/clairelcasey) | clairecasey#2710 | -| Eric Peterson | Spotify | Imaginary Goats | [iamEAP](http://github.com/iamEAP) | iamEAP#3058 | -| Harry Hogg | Spotify | Imaginary Goats | [HHogg](http://github.com/HHogg) | simplex#3451 | -| Joon Park | Spotify | Imaginary Goats | [Joonpark13](http://github.com/Joonpark13) | Sixpool#5060 | -| Lynette Lopez | Spotify | Imaginary Goats | [lynettelopez](https://github.com/lynettelopez) | lynettelopez | -| Mike Lewis | Spotify | Imaginary Goats | [mtlewis](http://github.com/mtlewis) | mtlewis#3658 | -| Tim Hansen | Spotify | Imaginary Goats | [timbonicus](http://github.com/timbonicus) | timbonicus#6871 | -| Vincenzo Scamporlino | Spotify | Imaginary Goats | [vinzscam](http://github.com/vinzscam) | vinzscam#6944 | +| Name | Organization | Team | GitHub | Discord | +| -------------------- | ------------ | --------------- | ------------------------------------------ | ------------- | +| Ainhoa Larumbe | Spotify | Imaginary Goats | [ainhoaL](http://github.com/ainhoaL) | ainhoa#8085 | +| Eric Peterson | Spotify | Imaginary Goats | [iamEAP](http://github.com/iamEAP) | iamEAP#3058 | +| Harry Hogg | Spotify | Imaginary Goats | [HHogg](http://github.com/HHogg) | simplex#3451 | +| Joon Park | Spotify | Imaginary Goats | [Joonpark13](http://github.com/Joonpark13) | Sixpool#5060 | +| Mike Lewis | Spotify | Imaginary Goats | [mtlewis](http://github.com/mtlewis) | mtlewis#3658 | +| Vincenzo Scamporlino | Spotify | Imaginary Goats | [vinzscam](http://github.com/vinzscam) | vinzscam#6944 | ### TechDocs @@ -127,9 +124,9 @@ Team: @backstage/openapi-tooling-maintainers Scope: Tooling for frontend and backend schema-first OpenAPI development. -| Name | Organization | GitHub | Discord | -| -------------- | ------------ | --------------------------------------- | ------------- | -| Aramis Sennyey | | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` | +| Name | Organization | GitHub | Discord | +| -------------- | ------------ | ----------------------------------------------------- | ------------- | +| Aramis Sennyey | | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` | ### Scaffolder @@ -157,7 +154,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. | Alex Crome | | [afscrome](https://github.com/afscrome) | `afscrome` | | Andre Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | | Andrew Thauer | Wealthsimple | [andrewthauer](https://github.com/andrewthauer) | `andrewthauer#3060` | -| Aramis Sennyey | | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` | +| Aramis Sennyey | | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` | | Brian Fletcher | Roadie.io | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` | | Carlos Esteban Lopez Jaramillo | VMWare | [luchillo17](https://github.com/luchillo17) | `luchillo17#8777` | | David Tuite | Roadie.io | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` | diff --git a/app-config.yaml b/app-config.yaml index 9b059da216..4b92687653 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -242,7 +242,7 @@ catalog: - Domain - Location providers: - openapi: + backstageOpenapi: plugins: - catalog - search @@ -399,6 +399,9 @@ auth: scopes: ${AUTH_ATLASSIAN_SCOPES} myproxy: development: {} + guest: + development: {} + costInsights: engineerCost: 200000 engineerThreshold: 0.5 diff --git a/beps/0003-auth-architecture-evolution/README.md b/beps/0003-auth-architecture-evolution/README.md index dc475fa14d..403390e567 100644 --- a/beps/0003-auth-architecture-evolution/README.md +++ b/beps/0003-auth-architecture-evolution/README.md @@ -68,9 +68,9 @@ Two new backend service interfaces are introduced to support these new features. The proposed design leaves the decision for how different endpoints are protected to the implementation of the plugin backends themselves. This includes whether particular routes should allow anonymous access, access from users authenticated via a cookie, or perhaps only allow access from other plugin backends and external services. This means that integrators do not need to - and do not have the ability to - configure access controls of individual endpoints, except for what the permission system already provides, and what is made available through static configuration or extension points. -In order to allow for cookie-based authentication of incoming user requests, the `auth` plugin backend is extended to be able to issue user tokens with reduced scope, which in turn integrate with the new `AuthService` and `HttpAuthService`. The ability to use cookie auth for requests is an opt-in per route and is only be permitted for read methods (`GET`, `HEAD`, `OPTIONS`). The actual implementation of cookie-based flows will be up to each plugin, but with significant help from the new auth service interfaces. +To ensure a secure-by-default design, there is a default access control policy that applies to all plugin routes, known as the "default auth policy". This policy is to only allow access from authenticated users and services, and is implemented in the `HttpRouterService` interface. In order to allow either unauthenticated access or cookie-based access, a plugin must opt-out of the default auth policy for specific path prefixes, effectively leaving the access control implementation to the plugin itself. This is done through the new `addAuthPolicy` method that is added to the `HttpRouterService` interface. -In order to allow either unauthenticated access or cookie-based access, a plugin must explicitly opt-in the specific path prefixes that these should be available at. This is done through a new method that is added to the `HttpRouterService` interface. +In order to allow for cookie-based authentication of incoming user requests, the `AuthService` is able to issue user tokens with limited scope. These limited scope tokens can still be used to fetch user information and in on-behalf-of service calls, but they are rejected by the default auth policy. The `HttpAuthService` provides a standardized way of handling cookies, which integrates with the `'user-cookie'` auth policy. The limited tokens can also be used in other contexts where it is beneficial to avoid storing full user credentials, but instead use credentials that can be upgraded in a controlled manner, such as scaffolder tasks. The `AuthService` implementation can choose to have a longer expiry of the limited tokens compared to the full user tokens, but this is not a requirement. For service-to-service communication we will move away from reusing user tokens in upstream requests. We will instead implement an "On-Behalf-Of" flow where incoming user credentials are encapsulated in a service token for the upstream request. In line with this the new auth service interfaces will aim to make it difficult to directly forward credentials from incoming requests, and instead encourage that plugin backends issue new service credentials for upstream requests. @@ -98,7 +98,7 @@ export type BackstageUserPrincipal = { }; export type BackstageServicePrincipal = { - type: 'user'; + type: 'service'; // Exact format TBD, possibly 'plugin:' or 'external:' subject: string; @@ -110,6 +110,8 @@ export type BackstageServicePrincipal = { export type BackstageCredentials = { $$type: '@backstage/BackstageCredentials'; + expiresAt?: Date; + principal: TPrincipal; }; @@ -120,13 +122,20 @@ export type BackstagePrincipalTypes = { }; export interface AuthService { - authenticate(token: string): Promise; + authenticate( + token: string, + options?: { + allowLimitedAccess?: boolean; + }, + ): Promise; isPrincipal( credentials: BackstageCredentials, type: TType, ): credentials is BackstageCredentials; + getNoneCredentials(): Promise>; + getOwnServiceCredentials(): Promise< BackstageCredentials >; @@ -135,6 +144,10 @@ export interface AuthService { onBehalfOf: BackstageCredentials; targetPluginId: string; }): Promise<{ token: string }>; + + getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ token: string; expiresAt: Date }>; } ``` @@ -149,6 +162,7 @@ export interface BackstageUserInfo { } export interface UserInfoService { + // The implementation of this method should support both regular and limited user credentials getUserInfo(credentials: BackstageCredentials): Promise; } ``` @@ -159,7 +173,7 @@ The `UserInfoService` is exported by `@backstage/auth-node`, and the initial imp > Open question: Should this instead be added to the `HttpAuthService`? It may fit a bit better there, but on the other hand it might make sense to add additional policies unrelated to authentication too, such as rate limiting. -The `HttpRouterService` interface will be extended with the ability to opt-out of the default protection of endpoints, enabling either cookie auth or unauthenticated access. +The `HttpRouterService` interface will be extended with the ability to opt-out of the default protection of endpoints, enabling cookie or unauthenticated access. ```ts export interface HttpRouterServiceAuthPolicy { @@ -201,7 +215,7 @@ export default createBackendPlugin({ This is expected to be the pattern for the vast majority of plugins. -#### A plugin with an endpoint that only allows cookie auth +#### A plugin with a cookie-based authentication endpoint ```ts export default createBackendPlugin({ @@ -209,11 +223,25 @@ export default createBackendPlugin({ register(env) { env.registerInit({ deps: { + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, http: coreServices.httpRouter, }, - async init({ http }) { + async init({ auth, httpAuth, http }) { + const router = Router(); + + // Endpoint that sets the cookie for the user + router.get('/cookie', async (req, res) => { + const { expiresAt } = await httpAuth.issueUserCookie(res); + + res.json({ expiresAt: expiresAt.toISOString() }); + }); + + // Endpoint protected by cookie auth + router.get('/static', express.static(/* ... */)); + // The order of these two calls does not matter - http.use(await createRouter(/* ... */)); + http.use(router); http.addAuthPolicy({ path: '/static', allow: 'user-cookie', @@ -224,7 +252,7 @@ export default createBackendPlugin({ }); ``` -#### A plugin that allows both public access and cookie auth +#### A plugin that disabled the default auth policy and handles auth by itself ```ts export default createBackendPlugin({ @@ -236,15 +264,8 @@ export default createBackendPlugin({ }, async init({ http }) { http.use(await createRouter(/* ... */)); - http.addAuthPolicy({ path: '/', - allow: 'user-cookie', - }); - - // Unauthenticated access takes precedence, the /public endpoint does not require cookie auth - http.addAuthPolicy({ - path: '/public', allow: 'unauthenticated', }); }, @@ -274,16 +295,19 @@ export interface HttpAuthService { req: Request, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise< BackstageCredentials >; - // The cookie issued by this method must be consumable by the `credentials` method, which in turn - // should create a credentials object that can be passed to the `getPluginRequestToken` method. - // The issued token must then in turn be a valid token for a user principal with full access. - issueUserCookie(res: Response): Promise; + issueUserCookie( + res: Response, + options?: { + // If credentials are not provided, they will be read from the request + credentials?: BackstageCredentials; + }, + ): Promise<{ expiresAt: Date }>; } ``` @@ -377,41 +401,22 @@ router.get('/read-data', (req, res) => { }); ``` -#### Issuing a cookie and allowing user cookie auth on a separate endpoint +#### Using limited user tokens to access user info ```ts -router.get('/cookie', async (req, res) => { - await httpAuth.issueUserCookie(res); // If this is a service call it'll throw - res.json({ ok: true }); +router.get('/read-data', (req, res) => { + const credentials = await httpAuth.credentials(req, { + allow: ['user'], + allowLimitedAccess: true, + }); + + const { userEntityRef, ownershipEntityRefs } = await userInfo.getUserInfo( + credentials, + ); + + console.log(`User ref=${userEntityRef} ownership=${ownershipEntityRefs}`); + // ... }); - -// Allowing cookie auth is a separate step where you call the addAuthPolicy method -// of the httpRouter API in your plugin setup code. -httpRouter.addAuthPolicy({ - path: '/static', - allow: 'user-cookie', -}); - -// Separate endpoint that serves static content, allowing user cookie auth as -// well as the default user and service auth methods -router.use('/static', express.static(staticContentDir)); -``` - -#### Passing along user identity from a cookie in an upstream request - -```ts -router.get( - '/read-data', - httpAuth.middleware({ allow: ['user-cookie'] }), - (req, res) => { - const credentials = await httpAuth.credentials(req, { allow: ['user'] }); - const { ownershipEntityRefs } = await userInfo.getUserInfo(credentials); - console.log( - `User ref=${credentials.userEntityRef} ownership=${ownershipEntityRefs}`, - ); - // ... - }, -); ``` ### Access Control Configuration @@ -425,6 +430,79 @@ backend: The exact impact that this has is that it disables the check in the `HttpRouterService` implementation, effectively applying the `unauthenticated` access level to all routes. Furthermore, it will also change `AuthService` so that the `getPluginRequestToken()` method will now issue an empty token for a `'none'` principal, rather than throwing. +### Token Details + +Note that this section is NOT normative. It illustrates the token shapes and major token flows that are involved in this proposal, but intentionally leaves out some low level details and is subject to change. + +#### Backstage Identity Tokens + +These are the regular tokens, commonly in short referred to just as "Backstage Tokens", that the auth backend generates for the user during sign-in. These are sent along with calls to backend plugins to identify the user. This BEP does not aim to change the shape of these tokens; this section is only here for informative purposes to convey what pieces of information that are at play. + +This is a JWT token. + +```yaml +# Header +{ + "alg": "ES256", + "kid": "4f5a0543-894a-4176-b0b7-699a7026b72f" +} +# Payload +{ + "iss": "http://localhost:7007/api/auth", + "sub": "user:default/example-user", + "ent": ["user:default/example-user", "group:default/my-team"], + "aud": "backstage", + "iat": 1708333140, + "exp": 1708336740 +} +``` + +The key ID is some random UUID. The `iss` (issuer) is the external base URL of your auth backend. Note that it uses the `ES256` asymmetric signature algorithm, and the auth backend exposes a JWKS that contains the public parts of the signing keys. The `sub` is an entity ref denoting who the signed in user is, and the `ent` is an array of entity refs that they claim ownership through. The `aud` (audience) is hardcoded to the string `"backstage"` always. + +#### Legacy Service Tokens + +These are the tokens that have been used for backend-to-backend communications before this BEP, and they will likely have changes as part of this work. + +This is a JWT token. + +```yaml +# Header +{ + "alg": "HS256" +} +# Payload +{ + "sub": "backstage-server", + "exp": 1708337056 +} +``` + +Note that unlike the identity token in the previous section, it uses the `HS256` symmetric signature algorithm. The key used is the first of the `backend.auth.keys` entries in your `app-config`, which is a shared secret among all backend plugins, enabling them to know that the caller is a legitimate one. But the token does not contain any information about who the caller is (it's just a generic `"backstage-server"`), nor who the receiver (audience) is. + +#### New Cookie Token Flow + +Some plugins serve static content that the browser engine requests directly, e.g. the TechDocs plugin. Those calls cannot easily have a bearer token attached to them. For these use cases a cookie based flow will be used instead. + +![Cookie token sequence diagram](./token-sequence-cookie.drawio.svg) + +The frontend part of the plugin ensures that a cookie endpoint on the backend part of the plugin is called before attempting to render static content. This endpoint validates the user's identity token and sets a corresponding cookie on the response. Subsequent requests for getting static content will automatically have this cookie attached to them by the browser. + +We intentionally do not specify here how the cookie token is acquired. It might be issued by the plugin itself or by the auth backend depending on how the architecture evolves, but this does not have any effect on plugin code. + +The cookie token contains the user's identifying information just like the identity cookie but is severely limited. It has the plugin itself specified as its audience. Thus, this token is not usable in any bearer token context, nor as a cookie toward any other plugin. + +#### New Service OBO Token Flow + +When a backend service needs to in turn make a request to another upstream service to fulfil the original request, it uses an On-Behalf-Of (OBO) token for the purpose. + +![OBO token sequence diagram](./token-sequence-obo.drawio.svg) + +The initial request in this picture is a frontend plugin, but the same concept applies if it is initiated by a service. The scaffolder backend in this example acquires an OBO token to be able to talk to the catalog plugin. + +We intentionally do not specify here how the OBO token is acquired. It might be issued by the plugin itself or by the auth backend depending on how the architecture evolves, but this does not have any effect on plugin code. + +The OBO token specifies the target service as its audience and itself as the subject, but additionally also contains the original caller's identifying information. Thus, the target service can identify who the nearest caller is but also apply permissions that are relevant to the original caller. The token is thus scoped to not be usable toward other backend plugins. + ## Release Plan The existing `IdentityService` and `TokenManagerService` will be deprecated and instead implemented in terms of the new `AuthService`. @@ -433,10 +511,11 @@ The new `AuthService` and `HttpAuthService` will need backwards compatible imple The backwards compatibility helpers will have the following behavior for each individual service call: -- `auth.authenticate(token)`: If the decoded token has the `backstage` audience, authenticate the token for a user principal using `identity.getIdentity(...)`, otherwise authenticate it using `tokenManager.authenticate(...)` and return a service principal with the subject `external:backstage-plugin`. If a no-op token manager is used then anything but a user token will be treated as a valid service token, which is consistent with existing behavior. +- `auth.authenticate(token, options)`: If the decoded token has the `backstage` audience, authenticate the token for a user principal using `identity.getIdentity(...)`, otherwise authenticate it using `tokenManager.authenticate(...)` and return a service principal with the subject `external:backstage-plugin`. If a no-op token manager is used then anything but a user token will be treated as a valid service token, which is consistent with existing behavior. The limited access option is ignored. - `auth.getOwnServiceCredentials()`: Use original implementation. - `auth.isPrincipal()`: Use original implementation. - `auth.getPluginRequestToken(options)`: Same behavior as the original implementation, using the `tokenManager` to issue service tokens, with the exception that a `none` principal will translate to an empty token rather than an error in order to properly forward calls with a no-op token manager. +- `auth.getLimitedUserToken(credentials)`: This is a no-op and returns the underlying user token with full scope. - `httpAuth.credentials(...)`: Use original implementation. - `httpAuth.issueUserCookie(...)`: This is a no-op as we do not need to support cookie auth in the legacy adapter. @@ -475,9 +554,9 @@ Cons: - Can be extremely confusing because the top-level middleware for more lax access will also apply to the more strict access levels. For example ```ts - const cookieRouter = Router(); - cookieRouter.use(rateLimit()); - http.useWithCookieAuthentication(cookieRouter); + const publicRouter = Router(); + publicRouter.use(rateLimit()); + http.useWithoutAuthentication(publicRouter); const mainRouter = Router(); // rateLimit() will apply here too @@ -487,7 +566,7 @@ Cons: This applied to any similar way of structuring this API, such as a single `.use()` method with additional options: ```ts -http.use(cookieRouter, { allow: ['user-cookie'] }); +http.use(publicRouter, { allow: ['unauthenticated'] }); ``` #### Separate configuration on different paths for `use` diff --git a/beps/0003-auth-architecture-evolution/token-sequence-cookie.drawio.svg b/beps/0003-auth-architecture-evolution/token-sequence-cookie.drawio.svg new file mode 100644 index 0000000000..b20b528c35 --- /dev/null +++ b/beps/0003-auth-architecture-evolution/token-sequence-cookie.drawio.svg @@ -0,0 +1,203 @@ + + + + + + + + +
+
+
+ cookie flow +
+
+
+
+ + cookie flow + +
+
+ + + + + +
+
+
+ Browser +
+
+
+
+ + Browser + +
+
+ + + + + + +
+
+
+ Techdocs +
+ Backend +
+
+
+
+ + Techdocs... + +
+
+ + + + + + +
+
+
+ GET /cookie +
+
+
+
+ + GET /cookie + +
+
+ + + + + +
+
+
+ set cookie on response +
+
+
+
+ + set cookie on response + +
+
+ + + + + +
+
+
+ browser static content requests +
+
+
+
+ + browser static content requests + +
+
+ + + + +
+
+
+ cookie token +
+
+
+
+ + cookie token + +
+
+ + + + +
+
+
+ cookie token +
+
+
+
+ + cookie token + +
+
+ + + + +
+
+
+ user identity token +
+
+
+
+ + user identity token + +
+
+ + + + + +
+
+
+ + acquire cookie token +
+ based on user token +
+
+
+
+
+
+ + acquire cookie token... + +
+
+ + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/beps/0003-auth-architecture-evolution/token-sequence-obo.drawio.svg b/beps/0003-auth-architecture-evolution/token-sequence-obo.drawio.svg new file mode 100644 index 0000000000..f337f8b572 --- /dev/null +++ b/beps/0003-auth-architecture-evolution/token-sequence-obo.drawio.svg @@ -0,0 +1,203 @@ + + + + + + + + +
+
+
+ Browser +
+
+
+
+ + Browser + +
+
+ + + + + + +
+
+
+ Scaffolder +
+ Backend +
+
+
+
+ + Scaffolder... + +
+
+ + + + + + +
+
+
+ + acquire service obo token +
+ based on user token +
+
+
+
+
+
+ + acquire service obo token... + +
+
+ + + + + +
+
+
+ POST /tasks +
+
+
+
+ + POST /tasks + +
+
+ + + + + +
+
+
+ success +
+
+
+
+ + success + +
+
+ + + + + +
+
+
+ Catalog +
+ Backend +
+
+
+
+ + Catalog... + +
+
+ + + + + + +
+
+
+ GET /entities/... +
+
+
+
+ + GET /entities/... + +
+
+ + + + + +
+
+
+ template entity +
+
+
+
+ + template entity + +
+
+ + + + +
+
+
+ user identity token +
+
+
+
+ + user identity token + +
+
+ + + + +
+
+
+ service obo token +
+
+
+
+ + service obo token + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/auth/cloudflare/access.md b/docs/auth/cloudflare/access.md index b1d8e4c676..509c0ba052 100644 --- a/docs/auth/cloudflare/access.md +++ b/docs/auth/cloudflare/access.md @@ -25,9 +25,14 @@ auth: providers: cfaccess: teamName: + serviceTokens: + - token: '1uh2fh19efvfh129f1f919u21f2f19jf2.access' + subject: 'bot-user@your-company.com' ``` -You can find the team name in the Cloudflare Zero Trust dashboard. +You can find the team name in the Cloudflare Zero Trust dashboard. The Service +Tokens section is optional -- you only need it if you have some Cloudflare +Service Tokens that you want to be able to log in to your Backstage instance. This config section must be in place for the provider to load at all. Now let's add the provider itself. diff --git a/docs/auth/guest/provider.md b/docs/auth/guest/provider.md new file mode 100644 index 0000000000..c730877a47 --- /dev/null +++ b/docs/auth/guest/provider.md @@ -0,0 +1,66 @@ +--- +id: provider +title: Guest Authentication Provider +sidebar_label: Guest +description: Adding a guest authentication provider in Backstage +--- + +Audience: Admins or developers + +## Summary + +The goal of this guide is to get you set up with a guest authentication provider that emits tokens. This is different than the old guest authentication that is purely stored on the frontend and does not have tokens. The main reason you'd want to use this provider is to use permissioned plugins. + +:::caution +This provider should only ever be enabled for `development`. To prevent unauthorized access to your data, this package is _explicitly_ disabled for non-development environments. +::: + +## Installation + +### Backend + +:::note +This will only work with the new backend system. There is no support for this in the old backend. +::: + +Add the `@backstage/plugin-auth-backend-module-guest-provider` to your backend installation. + +``` +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-guest-provider +``` + +Then, add it to your backend's `index.ts` file, + +```diff +const backend = createBackend(); + +backend.add('@backstage/plugin-auth-backend'); ++backend.add('@backstage/plugin-auth-backend-module-guest-provider'); + +await backend.start(); +``` + +### Frontend + +Add the following to your `SignInPage` providers, + +```diff +const providers = [ ++ 'guest', + ... +] +``` + +### Config + +Similar to the other authentication providers, you have to enable the provider in config. Add the following to your `app-config.local.yaml`, + +```diff +auth: + providers: ++ guest: ++ userEntityRef: user:default/guest ++ development: {} +``` + +We need to specify that the provider is enabled for the given environment, and as there are no config values for this provider yet, you can just specify an empty object. diff --git a/docs/backend-system/architecture/01-index.md b/docs/backend-system/architecture/01-index.md index 9c37df7b66..681d0fada2 100644 --- a/docs/backend-system/architecture/01-index.md +++ b/docs/backend-system/architecture/01-index.md @@ -56,8 +56,7 @@ Just like plugins, modules also have access to services and can depend on their ## Package structure A detailed explanation of the package architecture can be found in the -[Backstage Architecture -Overview](../../overview/architecture-overview.md#package-architecture). The +[Backstage Architecture Overview](../../overview/architecture-overview.md#package-architecture). The most important packages to consider for this system are the following: - `plugin--backend` houses the implementation of the backend plugins diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index 9eada30ef6..27e4eb7324 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -632,7 +632,7 @@ A basic installation of the events plugin looks as follows. ```ts title="packages/backend/src/index.ts" const backend = createBackend(); /* highlight-add-next-line */ -backend.add(import('@backstage/plugin-events-backend')); +backend.add(import('@backstage/plugin-events-backend/alpha')); ``` If you have other customizations made to `plugins/events.ts`, such as adding @@ -646,6 +646,7 @@ depends on the appropriate extension point and interacts with it. ```ts title="packages/backend/src/index.ts" /* highlight-add-start */ +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { createBackendModule } from '@backstage/backend-plugin-api'; /* highlight-add-end */ @@ -663,7 +664,28 @@ const eventsModuleCustomExtensions = createBackendModule({ async init({ events /* ..., other dependencies */ }) { // Here you have the opportunity to interact with the extension // point before the plugin itself gets instantiated - events.addSubscribers(new MySubscriber()); // just an example + events.addHttpPostIngress({ + // ... + }); + }, + }); + }, +}); +/* highlight-add-end */ + +/* highlight-add-start */ +const otherPluginModuleCustomExtensions = createBackendModule({ + pluginId: 'other-plugin', // name of the plugin that the module is targeting + moduleId: 'custom-extensions', + register(env) { + env.registerInit({ + deps: { + events: eventsServiceRef, + // ... and other dependencies as needed + }, + async init({ events /* ..., other dependencies */ }) { + // Here you have the opportunity to interact with the extension + // point before the plugin itself gets instantiated }, }); }, @@ -671,17 +693,11 @@ const eventsModuleCustomExtensions = createBackendModule({ /* highlight-add-end */ const backend = createBackend(); -backend.add(import('@backstage/plugin-events-backend')); +backend.add(import('@backstage/plugin-events-backend/alpha')); /* highlight-add-next-line */ backend.add(eventsModuleCustomExtensions()); -``` - -This also requires that you have a dependency on the corresponding node package, -if you didn't already have one. - -```bash -# from the repository root -yarn --cwd packages/backend add @backstage/plugin-events-node +/* highlight-add-next-line */ +backend.add(otherPluginModuleCustomExtensions()); ``` Here we've placed the module directly in the backend index file just to get diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index fec924fa91..72b7c20e07 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -451,7 +451,7 @@ You can generate an isomorphic plugin package by running:`yarn new --select plug or you can run `yarn new` and then select "plugin-common" from the list of options There's at this point no existing templates for generating isomorphic plugins -using the `@backstage/cli`. Perhaps the simplest wat to get started right now is +using the `@backstage/cli`. Perhaps the simplest way to get started right now is to copy the contents of one of the existing packages in the main repository, such as `plugins/scaffolder-common`, and rename the folder and file contents to the desired name. This example uses _foobar_ as the plugin name so the plugin diff --git a/docs/features/software-templates/writing-tests-for-actions.md b/docs/features/software-templates/writing-tests-for-actions.md new file mode 100644 index 0000000000..0c92811b6f --- /dev/null +++ b/docs/features/software-templates/writing-tests-for-actions.md @@ -0,0 +1,57 @@ +--- +id: writing-tests-for-actions +title: Writing Tests For Actions +description: How to write tests for actions +--- + +Once you created a new action, your own custom one, or you would like to contribute new actions, you have to cover it with +Unit tests to be sure that your actions do what they suppose to do. + +Make sure that you cover the most of scenario's, which could happen with the action. +One of indispensable part of the test is to supply the context to a handler of action for the execution. +We encourage you to use a utility method for that, so your tests are immune to structural changes of context. +What is inevitably going to happen during the time. + +Example how to use it: + +```typescript +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; + +const mockContext = createMockActionContext({ + input: { repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org' }, +}); + +await action.handler(mockContext); + +expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://dev.azure.com/organization/project/_git/repo', +); +``` + +One thing to be aware about: if you would like to call `createMockActionContext` inside `it`, +you have to provide a `workspacePath`. By default, `createMockActionContext` uses +`import { createMockDirectory } from '@backstage/backend-test-utils';` to create it for you. +This implementation contains a hook inside which creates this limitation. So in this case you can do then: + +```typescript +describe('github:autolinks:create', async () => { + const workspacePath = createMockDirectory().resolve('workspace'); + // ... + + it('should call the githubApis for creating alphanumeric autolink reference', async () => { + // ... + await action.handler( + createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + keyPrefix: 'TICKET-', + urlTemplate: 'https://example.com/TICKET?query=', + }, + workspacePath, + }), + ); + //... + }); +}); +``` diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index d6a13ca6e3..6ce70be063 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -4,56 +4,42 @@ title: Customize the look-and-feel of your App description: Documentation on Customizing look and feel of the App --- -Backstage ships with a default theme with a light and dark mode variant. The -themes are provided as a part of the -[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package, -which also includes utilities for customizing the default theme, or creating -completely new themes. +Backstage ships with a default theme with a light and dark mode variant. The themes are provided as a part of the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package, which also includes utilities for customizing the default theme, or creating completely new themes. ## Creating a Custom Theme -The easiest way to create a new theme is to use the `createTheme` function -exported by the -[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. You -can use it to override some basic parameters of the default theme such as the -color palette and font. +The easiest way to create a new theme is to use the `createUnifiedTheme` function exported by the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. You can use it to override some basic parameters of the default theme such as the color palette and font. -For example, you can create a new theme based on the default light theme like -this: +For example, you can create a new theme based on the default light theme like this: -```ts -import { createTheme, lightTheme } from '@backstage/theme'; +```ts title="packages/app/src/theme/myTheme.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; -const myTheme = createTheme({ - palette: lightTheme.palette, +const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + }), fontFamily: 'Comic Sans MS', defaultPageTheme: 'home', }); ``` -If you want more control over the theme, and for example customize font sizes -and margins, you can use the lower-level `createThemeOverrides` function -exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) -in combination with -[`createTheme`](https://material-ui.com/customization/theming/#createmuitheme-options-args-theme) -from [`@material-ui/core`](https://www.npmjs.com/package/@material-ui/core). See -the "Overriding Backstage and Material UI css rules" section below. +> Note: we recommend creating a `theme` folder in `packages/app/src` to place your theme file to keep things nicely organized. -You can also create a theme from scratch that matches the `BackstageTheme` type -exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). -See the -[Material UI docs on theming](https://material-ui.com/customization/theming/) -for more information about how that can be done. +You can also create a theme from scratch that matches the `BackstageTheme` type exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). See the +[Material UI docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done. ## Using your Custom Theme -To add a custom theme to your Backstage app, you pass it as configuration to -`createApp`. +To add a custom theme to your Backstage app, you pass it as configuration to `createApp`. -For example, adding the theme that we created in the previous section can be -done like this: +For example, adding the theme that we created in the previous section can be done like this: -```tsx +```tsx title="packages/app/src/App.tsx" import { createApp } from '@backstage/app-defaults'; import { ThemeProvider } from '@material-ui/core/styles'; import CssBaseline from '@material-ui/core/CssBaseline'; @@ -68,70 +54,68 @@ const app = createApp({ variant: 'light', icon: , Provider: ({ children }) => ( - - {children} - + ), }] }) ``` -Note that your list of custom themes overrides the default themes. If you still -want to use the default themes, they are exported as `lightTheme` and -`darkTheme` from -[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). +Note that your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.light` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). ## Example of a custom theme -```ts +```ts title="packages/app/src/theme/myTheme.ts" import { - createTheme, + createBaseThemeOptions, + createUnifiedTheme, genPageTheme, - lightTheme, + palettes, shapes, } from '@backstage/theme'; -const myTheme = createTheme({ - palette: { - ...lightTheme.palette, - primary: { - main: '#343b58', +const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: { + ...palettes.light, + primary: { + main: '#343b58', + }, + secondary: { + main: '#565a6e', + }, + error: { + main: '#8c4351', + }, + warning: { + main: '#8f5e15', + }, + info: { + main: '#34548a', + }, + success: { + main: '#485e30', + }, + background: { + default: '#d5d6db', + paper: '#d5d6db', + }, + banner: { + info: '#34548a', + error: '#8c4351', + text: '#343b58', + link: '#565a6e', + }, + errorBackground: '#8c4351', + warningBackground: '#8f5e15', + infoBackground: '#343b58', + navigation: { + background: '#343b58', + indicator: '#8f5e15', + color: '#d5d6db', + selectedColor: '#ffffff', + }, }, - secondary: { - main: '#565a6e', - }, - error: { - main: '#8c4351', - }, - warning: { - main: '#8f5e15', - }, - info: { - main: '#34548a', - }, - success: { - main: '#485e30', - }, - background: { - default: '#d5d6db', - paper: '#d5d6db', - }, - banner: { - info: '#34548a', - error: '#8c4351', - text: '#343b58', - link: '#565a6e', - }, - errorBackground: '#8c4351', - warningBackground: '#8f5e15', - infoBackground: '#343b58', - navigation: { - background: '#343b58', - indicator: '#8f5e15', - color: '#d5d6db', - selectedColor: '#ffffff', - }, - }, + }), defaultPageTheme: 'home', fontFamily: 'Comic Sans MS', /* below drives the header colors */ @@ -161,16 +145,92 @@ const myTheme = createTheme({ }); ``` -For a more complete example of a custom theme including Backstage and -Material UI component overrides, see the [Aperture -theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) -from the [Backstage demo site](https://demo.backstage.io). +For a more complete example of a custom theme including Backstage and Material UI component overrides, see the [Aperture theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) from the [Backstage demo site](https://demo.backstage.io). + +## Custom Typography + +When creating a custom theme you can also customize various aspects of the default typography, here's an example using simplified theme: + +```ts title="packages/app/src/theme/myTheme.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; + +const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + typography: { + htmlFontSize: 16, + fontFamily: 'Arial, sans-serif', + h1: { + fontSize: 54, + fontWeight: 700, + marginBottom: 10, + }, + h2: { + fontSize: 40, + fontWeight: 700, + marginBottom: 8, + }, + h3: { + fontSize: 32, + fontWeight: 700, + marginBottom: 6, + }, + h4: { + fontWeight: 700, + fontSize: 28, + marginBottom: 6, + }, + h5: { + fontWeight: 700, + fontSize: 24, + marginBottom: 4, + }, + h6: { + fontWeight: 700, + fontSize: 20, + marginBottom: 2, + }, + }, + defaultPageTheme: 'home', + }), +}); +``` + +If you wanted to only override a sub-set of the typography setting, for example just `h1` then you would do this: + +```ts title="packages/app/src/theme/myTheme.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + defaultTypography, + palettes, +} from '@backstage/theme'; + +const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + typography: { + ...defaultTypography, + htmlFontSize: 16, + fontFamily: 'Roboto, sans-serif', + h1: { + fontSize: 72, + fontWeight: 700, + marginBottom: 10, + }, + }, + defaultPageTheme: 'home', + }), +}); +``` ## Overriding Backstage and Material UI components styles -When creating a custom theme you would be applying different values to -component's css rules that use the theme object. For example, a Backstage -component's styles might look like this: +When creating a custom theme you would be applying different values to component's CSS rules that use the theme object. For example, a Backstage component's styles might look like this: ```tsx const useStyles = makeStyles( @@ -185,83 +245,50 @@ const useStyles = makeStyles( ); ``` -Notice how the `padding` is getting its value from `theme.spacing`, that means -that setting a value for spacing in your custom theme would affect this -component padding property and the same goes for `backgroundImage` which uses -`theme.page.backgroundImage`. However, the `boxShadow` property doesn't -reference any value from the theme, that means that creating a custom theme -wouldn't be enough to alter the `box-shadow` property or to add css rules that -aren't already defined like a margin. For these cases you should also create an -override. +Notice how the `padding` is getting its value from `theme.spacing`, that means that setting a value for spacing in your custom theme would affect this component padding property and the same goes for `backgroundImage` which uses `theme.page.backgroundImage`. However, the `boxShadow` property doesn't reference any value from the theme, that means that creating a custom theme wouldn't be enough to alter the `box-shadow` property or to add css rules that aren't already defined like a margin. For these cases you should also create an override. -```tsx -import { createApp } from '@backstage/core-app-api'; -import { BackstageTheme, lightTheme } from '@backstage/theme'; -/** - * The `@backstage/core-components` package exposes this type that - * contains all Backstage and `material-ui` components that can be - * overridden along with the classes key those components use. - */ -import { BackstageOverrides } from '@backstage/core-components'; +Here's how you would do that: -export const createCustomThemeOverrides = ( - theme: BackstageTheme, -): BackstageOverrides => { - return { +```ts title="packages/app/src/theme/myTheme.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; + +const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + }), + fontFamily: 'Comic Sans MS', + defaultPageTheme: 'home', + components: { BackstageHeader: { - header: { - width: 'auto', - margin: '20px', - boxShadow: 'none', - borderBottom: `4px solid ${theme.palette.primary.main}`, + styleOverrides: { + header: ({ theme }) => ({ + width: 'auto', + margin: '20px', + boxShadow: 'none', + borderBottom: `4px solid ${theme.palette.primary.main}`, + }), }, }, - }; -}; - -const customTheme: BackstageTheme = { - ...lightTheme, - overrides: { - // These are the overrides that Backstage applies to `material-ui` components - ...lightTheme.overrides, - // These are your custom overrides, either to `material-ui` or Backstage components. - ...createCustomThemeOverrides(lightTheme), }, -}; - -const app = createApp({ - apis: ..., - plugins: ..., - themes: [{ - id: 'my-theme', - title: 'My Custom Theme', - variant: 'light', - Provider: ({ children }) => ( - - {children} - - ), - }] }); ``` ## Custom Logo -In addition to a custom theme, you can also customize the logo displayed at the -far top left of the site. +In addition to a custom theme, you can also customize the logo displayed at the far top left of the site. -In your frontend app, locate `src/components/Root/` folder. You'll find two -components: +In your frontend app, locate `src/components/Root/` folder. You'll find two components: - `LogoFull.tsx` - A larger logo used when the Sidebar navigation is opened. -- `LogoIcon.tsx` - A smaller logo used when the sidebar navigation is closed. +- `LogoIcon.tsx` - A smaller logo used when the Sidebar navigation is closed. -To replace the images, you can simply replace the relevant code in those -components with raw SVG definitions. +To replace the images, you can simply replace the relevant code in those components with raw SVG definitions. -You can also use another web image format such as PNG by importing it. To do -this, place your new image into a new subdirectory such as -`src/components/Root/logo/my-company-logo.png`, and then add this code: +You can also use another web image format such as PNG by importing it. To do this, place your new image into a new subdirectory such as `src/components/Root/logo/my-company-logo.png`, and then add this code: ```tsx import MyCustomLogoFull from './logo/my-company-logo.png'; @@ -408,7 +435,7 @@ For this example we'll show you how you can expand the sidebar with a sub-menu: 3. Then update the `@backstage/core-components` import like this: - ```tsx + ```tsx title="packages/app/src/components/Root/Root.tsx" import { Sidebar, sidebarConfig, @@ -430,7 +457,7 @@ For this example we'll show you how you can expand the sidebar with a sub-menu: 4. Finally replace `` with this: - ```tsx + ```tsx title="packages/app/src/components/Root/Root.tsx" +- +- + +### Installation with Legacy Backend System + +#### Installation without Events Support And then add the entity provider to your catalog builder: @@ -49,7 +77,7 @@ export default async function createPlugin( } ``` -### Installation with Events Support +#### Installation with Events Support Please follow the installation instructions at @@ -83,6 +111,7 @@ export default async function createPlugin( env.config, { catalogApi: new CatalogClient({ discoveryApi: env.discovery }), + events: env.events, logger: env.logger, scheduler: env.scheduler, tokenManager: env.tokenManager, diff --git a/docs/overview/versioning-policy.md b/docs/overview/versioning-policy.md index 2ff3588af1..8b12138499 100644 --- a/docs/overview/versioning-policy.md +++ b/docs/overview/versioning-policy.md @@ -34,7 +34,7 @@ their own release cadence and versioning policy. Release cadence: Monthly, specifically on the Tuesday before the third Wednesday of each month. The first release took place in March 2022. -The main release line in versioned with a major, minor and patch version but +The main release line is versioned with a major, minor and patch version but does **not** adhere to [semver](https://semver.org). The version format is `..`, for example `1.3.0`. diff --git a/microsite/data/plugins/opa-permissions-wrapper.yaml b/microsite/data/plugins/opa-permissions-wrapper.yaml new file mode 100644 index 0000000000..64af859dbc --- /dev/null +++ b/microsite/data/plugins/opa-permissions-wrapper.yaml @@ -0,0 +1,10 @@ +--- +title: OPA Permissions Wrapper +author: Peter Macdonald +authorUrl: https://github.com/Parsifal-M +category: Authentication/Authorization +description: Manage your Backstage permissions with OPA (Open Policy Agent)! +documentation: https://github.com/Parsifal-M/backstage-opa-plugins/blob/main/plugins/permission-backend-module-opa-wrapper/README.md +iconUrl: /img/opapermlogo.png +npmPackageName: '@parsifal-m/plugin-permission-backend-module-opa-wrapper' +addedDate: '2024-02-26' diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 4f2724c0b6..155227f30a 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -116,6 +116,7 @@ "features/software-templates/input-examples", "features/software-templates/builtin-actions", "features/software-templates/writing-custom-actions", + "features/software-templates/writing-tests-for-actions", "features/software-templates/writing-custom-field-extensions", "features/software-templates/writing-custom-step-layouts", "features/software-templates/authorizing-parameters-steps-and-actions", @@ -307,6 +308,7 @@ "auth/gitlab/provider", "auth/google/provider", "auth/google/gcp-iap-auth", + "auth/guest/provider", "auth/okta/provider", "auth/oauth2-proxy/provider", "auth/onelogin/provider", diff --git a/microsite/static/img/opapermlogo.png b/microsite/static/img/opapermlogo.png new file mode 100644 index 0000000000..703e53dbf2 Binary files /dev/null and b/microsite/static/img/opapermlogo.png differ diff --git a/mkdocs.yml b/mkdocs.yml index 2a17613672..a2b2749233 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -161,6 +161,7 @@ nav: - GitLab: 'auth/gitlab/provider.md' - Google: 'auth/google/provider.md' - Google IAP: 'auth/google/gcp-iap-auth.md' + - Guest: 'auth/guest/provider.md' - OAuth2Proxy: 'auth/oauth2-proxy/provider.md' - Okta: 'auth/okta/provider.md' - OneLogin: 'auth/onelogin/provider.md' diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts index f24264930a..4600110b5c 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts @@ -28,10 +28,12 @@ import { BackstageServicePrincipal, BackstageUserPrincipal, } from '@backstage/backend-plugin-api'; +import { tokenManagerServiceFactory } from '../tokenManager'; // TODO: Ship discovery mock service in the service factory tester const mockDeps = [ discoveryServiceFactory(), + tokenManagerServiceFactory, mockServices.rootConfig.factory({ data: { backend: { diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 72d5635f61..78ff97a139 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ServerTokenManager, TokenManager } from '@backstage/backend-common'; +import { TokenManager } from '@backstage/backend-common'; import { AuthService, BackstageCredentials, @@ -27,10 +27,7 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; import { AuthenticationError } from '@backstage/errors'; -import { - DefaultIdentityClient, - IdentityApiGetIdentityRequest, -} from '@backstage/plugin-auth-node'; +import { IdentityApiGetIdentityRequest } from '@backstage/plugin-auth-node'; import { decodeJwt } from 'jose'; /** @internal */ @@ -38,7 +35,6 @@ export type InternalBackstageCredentials = BackstageCredentials & { version: string; token?: string; - authMethod: 'token' | 'cookie' | 'none'; }; export function createCredentialsWithServicePrincipal( @@ -51,24 +47,23 @@ export function createCredentialsWithServicePrincipal( type: 'service', subject: sub, }, - authMethod: 'token', }; } export function createCredentialsWithUserPrincipal( sub: string, token: string, - authMethod: 'token' | 'cookie' = 'token', + expiresAt?: Date, ): InternalBackstageCredentials { return { $$type: '@backstage/BackstageCredentials', version: 'v1', token, + expiresAt, principal: { type: 'user', userEntityRef: sub, }, - authMethod, }; } @@ -79,7 +74,6 @@ export function createCredentialsWithNonePrincipal(): InternalBackstageCredentia principal: { type: 'none', }, - authMethod: 'none', }; } @@ -114,6 +108,7 @@ class DefaultAuthService implements AuthService { private readonly disableDefaultAuthPolicy: boolean, ) {} + // allowLimitedAccess is currently ignored, since we currently always use the full user tokens async authenticate(token: string): Promise { const { sub, aud } = decodeJwt(token); @@ -137,6 +132,7 @@ class DefaultAuthService implements AuthService { return createCredentialsWithUserPrincipal( identity.identity.userEntityRef, token, + this.#getJwtExpiration(token), ); } @@ -159,6 +155,12 @@ class DefaultAuthService implements AuthService { return true; } + async getNoneCredentials(): Promise< + BackstageCredentials + > { + return createCredentialsWithNonePrincipal(); + } + async getOwnServiceCredentials(): Promise< BackstageCredentials > { @@ -196,6 +198,30 @@ class DefaultAuthService implements AuthService { ); } } + + async getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ token: string; expiresAt: Date }> { + const internalCredentials = toInternalBackstageCredentials(credentials); + + const { token } = internalCredentials; + + if (!token) { + throw new AuthenticationError( + 'User credentials is unexpectedly missing token', + ); + } + + return { token, expiresAt: this.#getJwtExpiration(token) }; + } + + #getJwtExpiration(token: string) { + const { exp } = decodeJwt(token); + if (!exp) { + throw new AuthenticationError('User token is missing expiration'); + } + return new Date(exp * 1000); + } } /** @public */ @@ -204,14 +230,15 @@ export const authServiceFactory = createServiceFactory({ deps: { config: coreServices.rootConfig, logger: coreServices.rootLogger, - discovery: coreServices.discovery, plugin: coreServices.pluginMetadata, + identity: coreServices.identity, + // Re-using the token manager makes sure that we use the same generated keys for + // development as plugins that have not yet been migrated. It's important that this + // keeps working as long as there are plugins that have not been migrated to the + // new auth services in the new backend system. + tokenManager: coreServices.tokenManager, }, - createRootContext({ config, logger }) { - return ServerTokenManager.fromConfig(config, { logger }); - }, - async factory({ discovery, config, plugin }, tokenManager) { - const identity = DefaultIdentityClient.create({ discovery }); + async factory({ config, plugin, identity, tokenManager }) { const disableDefaultAuthPolicy = Boolean( config.getOptionalBoolean( 'backend.auth.dangerouslyDisableDefaultAuthPolicy', diff --git a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts index 1bd9d3cf2a..db36e5bf2a 100644 --- a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts @@ -18,6 +18,7 @@ import { AuthService, BackstageCredentials, BackstagePrincipalTypes, + BackstageUserPrincipal, DiscoveryService, HttpAuthService, coreServices, @@ -26,11 +27,8 @@ import { import { AuthenticationError, NotAllowedError } from '@backstage/errors'; import { parse as parseCookie } from 'cookie'; import { Request, Response } from 'express'; -import { decodeJwt } from 'jose'; -import { - createCredentialsWithNonePrincipal, - toInternalBackstageCredentials, -} from '../auth/authServiceFactory'; + +const FIVE_MINUTES_MS = 5 * 60 * 1000; const BACKSTAGE_AUTH_COOKIE = 'backstage-auth'; @@ -41,54 +39,78 @@ function getTokenFromRequest(req: Request) { const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i); const token = matches?.[1]; if (token) { - return { token, isCookie: false }; + return token; } } + return undefined; +} + +function getCookieFromRequest(req: Request) { const cookieHeader = req.headers.cookie; if (cookieHeader) { const cookies = parseCookie(cookieHeader); const token = cookies[BACKSTAGE_AUTH_COOKIE]; if (token) { - return { token, isCookie: true }; + return token; } } - return { token: undefined, isCookie: false }; + return undefined; +} + +function willExpireSoon(expiresAt: Date) { + return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime(); } const credentialsSymbol = Symbol('backstage-credentials'); +const limitedCredentialsSymbol = Symbol('backstage-limited-credentials'); type RequestWithCredentials = Request & { [credentialsSymbol]?: Promise; + [limitedCredentialsSymbol]?: Promise; }; class DefaultHttpAuthService implements HttpAuthService { + readonly #auth: AuthService; + readonly #discovery: DiscoveryService; + readonly #pluginId: string; + constructor( - private readonly auth: AuthService, - private readonly discovery: DiscoveryService, - private readonly pluginId: string, - ) {} + auth: AuthService, + discovery: DiscoveryService, + pluginId: string, + ) { + this.#auth = auth; + this.#discovery = discovery; + this.#pluginId = pluginId; + } async #extractCredentialsFromRequest(req: Request) { - const { token, isCookie } = getTokenFromRequest(req); + const token = getTokenFromRequest(req); if (!token) { - return createCredentialsWithNonePrincipal(); + return await this.#auth.getNoneCredentials(); } - const credentials = toInternalBackstageCredentials( - await this.auth.authenticate(token), - ); - if (isCookie) { - if (credentials.principal.type !== 'user') { - throw new AuthenticationError( - 'Refusing to authenticate non-user principal with cookie auth', - ); - } - credentials.authMethod = 'cookie'; + return await this.#auth.authenticate(token); + } + + async #extractLimitedCredentialsFromRequest(req: Request) { + const token = getTokenFromRequest(req); + if (token) { + return await this.#auth.authenticate(token, { + allowLimitedAccess: true, + }); } - return credentials; + const cookie = getCookieFromRequest(req); + if (cookie) { + return await this.#auth.authenticate(cookie, { + allowLimitedAccess: true, + }); + } + + return await this.#auth.getNoneCredentials(); } async #getCredentials(req: RequestWithCredentials) { @@ -96,73 +118,131 @@ class DefaultHttpAuthService implements HttpAuthService { this.#extractCredentialsFromRequest(req)); } + async #getLimitedCredentials(req: RequestWithCredentials) { + return (req[limitedCredentialsSymbol] ??= + this.#extractLimitedCredentialsFromRequest(req)); + } + async credentials( req: Request, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise> { - const credentials = toInternalBackstageCredentials( - await this.#getCredentials(req), - ); + // Limited and full credentials are treated as two separate cases, this lets + // us avoid internal dependencies between the AuthService and + // HttpAuthService implementations + const credentials = options?.allowLimitedAccess + ? await this.#getLimitedCredentials(req) + : await this.#getCredentials(req); - const allowedPrincipalTypes = options?.allow; - const allowedAuthMethods: Array<'token' | 'cookie' | 'none'> = - options?.allowedAuthMethods ?? ['token']; - - if ( - credentials.authMethod !== 'none' && - !allowedAuthMethods.includes(credentials.authMethod) - ) { - throw new NotAllowedError( - `This endpoint does not allow the '${credentials.authMethod}' auth method`, - ); + const allowed = options?.allow; + if (!allowed) { + return credentials as any; } - if ( - allowedPrincipalTypes && - !allowedPrincipalTypes.includes(credentials.principal.type as TAllowed) - ) { - if (credentials.authMethod === 'none') { - throw new AuthenticationError(); + if (this.#auth.isPrincipal(credentials, 'none')) { + if (allowed.includes('none' as TAllowed)) { + return credentials as any; } + + throw new AuthenticationError('Missing credentials'); + } else if (this.#auth.isPrincipal(credentials, 'user')) { + if (allowed.includes('user' as TAllowed)) { + return credentials as any; + } + throw new NotAllowedError( - `This endpoint does not allow '${credentials.principal.type}' credentials`, + `This endpoint does not allow 'user' credentials`, + ); + } else if (this.#auth.isPrincipal(credentials, 'service')) { + if (allowed.includes('service' as TAllowed)) { + return credentials as any; + } + + throw new NotAllowedError( + `This endpoint does not allow 'service' credentials`, ); } - return credentials as any; + throw new NotAllowedError( + 'Unknown principal type, this should never happen', + ); } - async issueUserCookie(res: Response): Promise { - const credentials = await this.credentials(res.req, { allow: ['user'] }); + async issueUserCookie( + res: Response, + options?: { credentials?: BackstageCredentials }, + ): Promise<{ expiresAt: Date }> { + if (res.headersSent) { + throw new Error('Failed to issue user cookie, headers were already sent'); + } + + let credentials: BackstageCredentials; + if (options?.credentials) { + if (!this.#auth.isPrincipal(options.credentials, 'user')) { + throw new AuthenticationError( + 'Refused to issue cookie for non-user principal', + ); + } + credentials = options.credentials; + } else { + credentials = await this.credentials(res.req, { allow: ['user'] }); + } + + const existingExpiresAt = await this.#existingCookieExpiration(res.req); + if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) { + return { expiresAt: existingExpiresAt }; + } + + const originHeader = res.req.headers.origin; + const origin = + !originHeader || originHeader === 'null' ? undefined : originHeader; // https://backstage.example.com/api/catalog - const externalBaseUrlStr = await this.discovery.getExternalBaseUrl( - this.pluginId, + const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl( + this.#pluginId, ); - const externalBaseUrl = new URL(externalBaseUrlStr); + const externalBaseUrl = new URL(origin ?? externalBaseUrlStr); - const { token } = toInternalBackstageCredentials(credentials); + const { token, expiresAt } = await this.#auth.getLimitedUserToken( + credentials, + ); if (!token) { throw new Error('User credentials is unexpectedly missing token'); } - // TODO: Proper refresh and expiration handling - const expires = decodeJwt(token).exp!; + const secure = + externalBaseUrl.protocol === 'https:' || + externalBaseUrl.hostname === 'localhost'; - // TODO: refresh this thing res.cookie(BACKSTAGE_AUTH_COOKIE, token, { domain: externalBaseUrl.hostname, httpOnly: true, - expires: new Date(expires * 1000), - path: externalBaseUrl.pathname, + expires: expiresAt, + secure, priority: 'high', - sameSite: 'lax', // TBD + sameSite: secure ? 'none' : 'lax', }); - throw new Error('Method not implemented.'); + return { expiresAt }; + } + + async #existingCookieExpiration(req: Request): Promise { + const existingCookie = getCookieFromRequest(req); + if (!existingCookie) { + return undefined; + } + + const existingCredentials = await this.#auth.authenticate(existingCookie, { + allowLimitedAccess: true, + }); + if (!this.#auth.isPrincipal(existingCredentials, 'user')) { + return undefined; + } + + return existingCredentials.expiresAt; } } diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts index b8430d398e..a5246c91b2 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts @@ -53,7 +53,10 @@ describe('createCredentialsBarrier', () => { .expect(401) .expect(res => expect(res.body).toMatchObject({ - error: { name: 'AuthenticationError', message: '' }, + error: { + name: 'AuthenticationError', + message: 'Missing credentials', + }, }), ); @@ -98,7 +101,7 @@ describe('createCredentialsBarrier', () => { .expect(200); }); - it('should allow exceptions to the default auth policy to be made', async () => { + it('should allow exceptions for unauthenticated access', async () => { const { app, barrier } = setup(); await request(app).get('/').send().expect(401); @@ -118,5 +121,55 @@ describe('createCredentialsBarrier', () => { await request(app).get('/other').send().expect(200); }); - // TODO: cookie auth + it('should allow exceptions for cookie access', async () => { + const { app, barrier } = setup(); + + await request(app).get('/').send().expect(401); + await request(app).get('/public').send().expect(401); + await request(app).get('/other').send().expect(401); + await request(app) + .get('/static') + .set('cookie', mockCredentials.limitedUser.cookie()) + .send() + .expect(401); + await request(app) + .get('/static') + .set('authorization', mockCredentials.user.header()) + .send() + .expect(200); + + barrier.addAuthPolicy({ allow: 'user-cookie', path: '/static' }); + + await request(app).get('/').send().expect(401); + await request(app).get('/static').send().expect(401); + await request(app) + .get('/static') + .set('cookie', mockCredentials.limitedUser.cookie()) + .send() + .expect(200); + await request(app) + .get('/static') + .set('authorization', mockCredentials.user.header()) + .send() + .expect(200); + + await request(app).get('/other').send().expect(401); + + // Unauthenticated access should take precedence + barrier.addAuthPolicy({ allow: 'unauthenticated', path: '/' }); + + await request(app).get('/').send().expect(200); + await request(app).get('/static').send().expect(200); + await request(app) + .get('/static') + .set('cookie', mockCredentials.limitedUser.cookie()) + .send() + .expect(200); + await request(app) + .get('/static') + .set('cookie', mockCredentials.limitedUser.invalidCookie()) + .send() + .expect(200); + await request(app).get('/other').send().expect(200); + }); }); diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts index a512c5ac9e..a69fa5a804 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts @@ -76,7 +76,7 @@ export function createCredentialsBarrier(options: { httpAuth .credentials(req, { allow: ['user', 'service'], - allowedAuthMethods: allowsCookie ? ['token', 'cookie'] : ['token'], + allowLimitedAccess: allowsCookie, }) .then( () => next(), diff --git a/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts b/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts index a74b8b7002..7d3a2af7b5 100644 --- a/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts @@ -43,8 +43,11 @@ export class DefaultUserInfoService implements UserInfoService { if (typeof userEntityRef !== 'string') { throw new Error('User entity ref must be a string'); } - if (!Array.isArray(ownershipEntityRefs)) { - throw new Error('Ownership entity refs must be an array'); + if ( + !Array.isArray(ownershipEntityRefs) || + ownershipEntityRefs.some(ref => typeof ref !== 'string') + ) { + throw new Error('Ownership entity refs must be an array of strings'); } return { userEntityRef, ownershipEntityRefs }; diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 1940a79b78..55cb3b34dc 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -67,6 +67,7 @@ import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService as TokenManager } from '@backstage/backend-plugin-api'; import { TransportStreamOptions } from 'winston-transport'; import { UrlReaderService as UrlReader } from '@backstage/backend-plugin-api'; +import { UserInfoService } from '@backstage/backend-plugin-api'; import { V1PodTemplateSpec } from '@kubernetes/client-node'; import * as winston from 'winston'; import { Writable } from 'stream'; @@ -239,30 +240,32 @@ export function createLegacyAuthAdapters< TOptions extends { auth?: AuthService; httpAuth?: HttpAuthService; + userInfo?: UserInfoService; identity?: IdentityService; tokenManager?: TokenManager; discovery: PluginEndpointDiscovery; }, - TAdapters = TOptions extends { + TAdapters = (TOptions extends { auth?: AuthService; } - ? TOptions extends { - httpAuth?: HttpAuthService; + ? { + auth: AuthService; } + : {}) & + (TOptions extends { + httpAuth?: HttpAuthService; + } ? { - auth: AuthService; httpAuth: HttpAuthService; } - : { - auth: AuthService; + : {}) & + (TOptions extends { + userInfo?: UserInfoService; + } + ? { + userInfo: UserInfoService; } - : TOptions extends { - httpAuth?: HttpAuthService; - } - ? { - httpAuth: HttpAuthService; - } - : 'error: at least one of auth and/or httpAuth must be provided', + : {}), >(options: TOptions): TAdapters; // @public diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 2fe5945750..f60502ecd1 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -101,10 +101,10 @@ "pg": "^8.11.3", "raw-body": "^2.4.1", "tar": "^6.1.12", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1", "winston-transport": "^4.5.0", - "yauzl": "^2.10.0", + "yauzl": "^3.0.0", "yn": "^4.0.0" }, "peerDependencies": { diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts index 7e1f1be858..43a503199b 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts @@ -56,7 +56,22 @@ describe('createLegacyAuthAdapters', () => { expect(ret.httpAuth).toBe(httpAuth); }); - it('should adapt both auth and httpAuth if neither are provided', () => { + it('should pass through userInfo if it is provided', () => { + const auth = {}; + const userInfo = {}; + const ret = createLegacyAuthAdapters({ + auth: auth as any, + userInfo: userInfo as any, + tokenManager: mockServices.tokenManager(), + discovery: {} as any, + identity: mockServices.identity(), + }); + + expect(ret.auth).toBe(auth); + expect(ret.userInfo).toBe(userInfo); + }); + + it('should adapt all services if none are provided', () => { const ret = createLegacyAuthAdapters({ auth: undefined, httpAuth: undefined, @@ -68,6 +83,7 @@ describe('createLegacyAuthAdapters', () => { expect(ret).toEqual({ auth: expect.any(Object), httpAuth: expect.any(Object), + userInfo: expect.any(Object), }); }); }); diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts index 98d21ba559..a46d553c5d 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts @@ -17,12 +17,15 @@ import { AuthService, BackstageCredentials, + BackstageNonePrincipal, BackstagePrincipalTypes, BackstageServicePrincipal, + BackstageUserInfo, BackstageUserPrincipal, HttpAuthService, IdentityService, TokenManagerService, + UserInfoService, } from '@backstage/backend-plugin-api'; import { ServerTokenManager, TokenManager } from '../tokens'; import { AuthenticationError, NotAllowedError } from '@backstage/errors'; @@ -63,6 +66,12 @@ class AuthCompat implements AuthService { return true; } + async getNoneCredentials(): Promise< + BackstageCredentials + > { + return createCredentialsWithNonePrincipal(); + } + async getOwnServiceCredentials(): Promise< BackstageCredentials > { @@ -87,6 +96,7 @@ class AuthCompat implements AuthService { return createCredentialsWithUserPrincipal( identity.identity.userEntityRef, token, + this.#getJwtExpiration(token), ); } @@ -121,6 +131,30 @@ class AuthCompat implements AuthService { ); } } + + async getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ token: string; expiresAt: Date }> { + const internalCredentials = toInternalBackstageCredentials(credentials); + + const { token } = internalCredentials; + + if (!token) { + throw new AuthenticationError( + 'User credentials is unexpectedly missing token', + ); + } + + return { token, expiresAt: this.#getJwtExpiration(token) }; + } + + #getJwtExpiration(token: string) { + const { exp } = decodeJwt(token); + if (!exp) { + throw new AuthenticationError('User token is missing expiration'); + } + return new Date(exp * 1000); + } } function getTokenFromRequest(req: Request) { @@ -144,7 +178,11 @@ type RequestWithCredentials = Request & { }; class HttpAuthCompat implements HttpAuthService { - constructor(private readonly auth: AuthService) {} + #auth: AuthService; + + constructor(auth: AuthService) { + this.#auth = auth; + } async #extractCredentialsFromRequest(req: Request) { const token = getTokenFromRequest(req); @@ -153,7 +191,7 @@ class HttpAuthCompat implements HttpAuthService { } const credentials = toInternalBackstageCredentials( - await this.auth.authenticate(token), + await this.#auth.authenticate(token), ); return credentials; @@ -168,39 +206,79 @@ class HttpAuthCompat implements HttpAuthService { req: Request, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise> { const credentials = toInternalBackstageCredentials( await this.#getCredentials(req), ); - const allowedPrincipalTypes = options?.allow; - const allowedAuthMethods: Array<'token' | 'cookie' | 'none'> = - options?.allowedAuthMethods ?? ['token']; + const allowed = options?.allow; + if (!allowed) { + return credentials as any; + } + + if (this.#auth.isPrincipal(credentials, 'none')) { + if (allowed.includes('none' as TAllowed)) { + return credentials as any; + } + + throw new AuthenticationError('Missing credentials'); + } else if (this.#auth.isPrincipal(credentials, 'user')) { + if (allowed.includes('user' as TAllowed)) { + return credentials as any; + } - if ( - credentials.authMethod !== 'none' && - !allowedAuthMethods.includes(credentials.authMethod) - ) { throw new NotAllowedError( - `This endpoint does not allow the '${credentials.authMethod}' auth method`, + `This endpoint does not allow 'user' credentials`, + ); + } else if (this.#auth.isPrincipal(credentials, 'service')) { + if (allowed.includes('service' as TAllowed)) { + return credentials as any; + } + + throw new NotAllowedError( + `This endpoint does not allow 'service' credentials`, ); } - if ( - allowedPrincipalTypes && - !allowedPrincipalTypes.includes(credentials.principal.type as TAllowed) - ) { - throw new NotAllowedError( - `This endpoint does not allow '${credentials.principal.type}' credentials`, - ); - } - - return credentials as any; + throw new NotAllowedError( + 'Unknown principal type, this should never happen', + ); } - async issueUserCookie(_res: Response): Promise {} + async issueUserCookie(_res: Response): Promise<{ expiresAt: Date }> { + return { expiresAt: new Date(Date.now() + 3600_000) }; + } +} + +export class UserInfoCompat implements UserInfoService { + async getUserInfo( + credentials: BackstageCredentials, + ): Promise { + const internalCredentials = toInternalBackstageCredentials(credentials); + if (internalCredentials.principal.type !== 'user') { + throw new Error('Only user credentials are supported'); + } + if (!internalCredentials.token) { + throw new Error('User credentials is unexpectedly missing token'); + } + const { sub: userEntityRef, ent: ownershipEntityRefs = [] } = decodeJwt( + internalCredentials.token, + ); + + if (typeof userEntityRef !== 'string') { + throw new Error('User entity ref must be a string'); + } + if ( + !Array.isArray(ownershipEntityRefs) || + ownershipEntityRefs.some(ref => typeof ref !== 'string') + ) { + throw new Error('Ownership entity refs must be an array of strings'); + } + + return { userEntityRef, ownershipEntityRefs }; + } } /** @@ -211,38 +289,47 @@ export function createLegacyAuthAdapters< TOptions extends { auth?: AuthService; httpAuth?: HttpAuthService; + userInfo?: UserInfoService; identity?: IdentityService; tokenManager?: TokenManager; discovery: PluginEndpointDiscovery; }, - TAdapters = TOptions extends { - auth?: AuthService; - } - ? TOptions extends { httpAuth?: HttpAuthService } - ? { auth: AuthService; httpAuth: HttpAuthService } - : { auth: AuthService } - : TOptions extends { httpAuth?: HttpAuthService } - ? { httpAuth: HttpAuthService } - : 'error: at least one of auth and/or httpAuth must be provided', + TAdapters = (TOptions extends { auth?: AuthService } + ? { auth: AuthService } + : {}) & + (TOptions extends { httpAuth?: HttpAuthService } + ? { httpAuth: HttpAuthService } + : {}) & + (TOptions extends { userInfo?: UserInfoService } + ? { userInfo: UserInfoService } + : {}), >(options: TOptions): TAdapters { - const { auth, httpAuth, discovery } = options; + const { + auth, + httpAuth, + userInfo = new UserInfoCompat(), + discovery, + } = options; if (auth && httpAuth) { return { auth, httpAuth, + userInfo, } as TAdapters; } if (auth) { return { auth, + userInfo, } as TAdapters; } if (httpAuth) { return { httpAuth, + userInfo, } as TAdapters; } @@ -257,5 +344,6 @@ export function createLegacyAuthAdapters< return { auth: authImpl, httpAuth: httpAuthImpl, + userInfo, } as TAdapters; } diff --git a/packages/backend-common/src/middleware/errorHandler.test.ts b/packages/backend-common/src/middleware/errorHandler.test.ts index 3cc473daa0..ab07a91022 100644 --- a/packages/backend-common/src/middleware/errorHandler.test.ts +++ b/packages/backend-common/src/middleware/errorHandler.test.ts @@ -21,11 +21,13 @@ import { NotAllowedError, NotFoundError, NotModifiedError, + ResponseError, } from '@backstage/errors'; import express from 'express'; import createError from 'http-errors'; import request from 'supertest'; import { errorHandler } from './errorHandler'; +import { STATUS_CODES } from 'http'; describe('errorHandler', () => { it('gives default code and message', async () => { @@ -116,6 +118,53 @@ describe('errorHandler', () => { app.use('/ConflictError', () => { throw new ConflictError(); }); + app.use('/ResponseErrorBackstagePlugin', async (_req, _res, next) => { + const mockedResponse = { + status: jest.fn(() => mockedResponse), + json: jest.fn(() => mockedResponse), + } as unknown as jest.Mocked; + + // serialize AuthenticationError in mockedResponse + errorHandler()( + new AuthenticationError('an error'), + { method: 'GET', url: '' } as express.Request, + mockedResponse, + jest.fn(), + ); + + const status = mockedResponse.status.mock.calls[0][0]; + next( + await ResponseError.fromResponse({ + headers: new Headers({ + 'content-type': 'application/json', + }), + ok: false, + redirected: false, + status, + statusText: STATUS_CODES[status]!, + type: 'default', + url: '', + text: async () => + JSON.stringify(mockedResponse.json.mock.calls[0][0]), + }), + ); + }); + app.use('/ResponseError', async (_req, _res, next) => { + next( + await ResponseError.fromResponse({ + headers: new Headers({ + 'content-type': 'application/json', + }), + ok: false, + redirected: false, + status: 403, + statusText: STATUS_CODES[403]!, + type: 'default', + url: '', + text: async () => JSON.stringify({}), + }), + ); + }); app.use(errorHandler()); const r = request(app); @@ -138,6 +187,14 @@ describe('errorHandler', () => { expect((await r.get('/ConflictError')).body.error.name).toBe( 'ConflictError', ); + expect((await r.get('/ResponseErrorBackstagePlugin')).status).toBe(401); + expect((await r.get('/ResponseErrorBackstagePlugin')).body.error.name).toBe( + 'ResponseError', + ); + expect((await r.get('/ResponseError')).status).toBe(403); + expect((await r.get('/ResponseError')).body.error.name).toBe( + 'ResponseError', + ); }); it('logs all 500 errors', async () => { diff --git a/packages/backend-dynamic-feature-service/api-report.md b/packages/backend-dynamic-feature-service/api-report.md index 64bffb468c..8075acabfb 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -10,6 +10,7 @@ import { Config } from '@backstage/config'; import { ConfigSchema } from '@backstage/config-loader'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventsBackend } from '@backstage/plugin-events-backend'; +import { EventsService } from '@backstage/plugin-events-node'; import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { IdentityApi } from '@backstage/plugin-auth-node'; @@ -215,6 +216,7 @@ export type LegacyPluginEnvironment = { scheduler: PluginTaskScheduler; identity: IdentityApi; eventBroker: EventBroker; + events: EventsService; pluginProvider: BackendPluginProvider; }; diff --git a/packages/backend-dynamic-feature-service/src/manager/types.ts b/packages/backend-dynamic-feature-service/src/manager/types.ts index 0038470686..60b2eb831e 100644 --- a/packages/backend-dynamic-feature-service/src/manager/types.ts +++ b/packages/backend-dynamic-feature-service/src/manager/types.ts @@ -29,6 +29,7 @@ import { IdentityApi } from '@backstage/plugin-auth-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { EventBroker, + EventsService, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; @@ -64,6 +65,7 @@ export type LegacyPluginEnvironment = { scheduler: PluginTaskScheduler; identity: IdentityApi; eventBroker: EventBroker; + events: EventsService; pluginProvider: BackendPluginProvider; }; diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index ae7f33c88e..49e3df2db9 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -33,6 +33,7 @@ "@backstage/plugin-app-backend": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-azure-devops-backend": "workspace:^", "@backstage/plugin-badges-backend": "workspace:^", diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index 57a63adf8a..4965462b2c 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -20,6 +20,7 @@ const backend = createBackend(); backend.add(import('@backstage/plugin-auth-backend')); backend.add(import('./authModuleGithubProvider')); +backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); backend.add(import('@backstage/plugin-adr-backend')); backend.add(import('@backstage/plugin-app-backend/alpha')); diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index cb44ba158d..73c1d15cd1 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -24,7 +24,21 @@ import { Response as Response_2 } from 'express'; // @public (undocumented) export interface AuthService { // (undocumented) - authenticate(token: string): Promise; + authenticate( + token: string, + options?: { + allowLimitedAccess?: boolean; + }, + ): Promise; + // (undocumented) + getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ + token: string; + expiresAt: Date; + }>; + // (undocumented) + getNoneCredentials(): Promise>; // (undocumented) getOwnServiceCredentials(): Promise< BackstageCredentials @@ -107,6 +121,7 @@ export interface BackendPluginRegistrationPoints { // @public (undocumented) export type BackstageCredentials = { $$type: '@backstage/BackstageCredentials'; + expiresAt?: Date; principal: TPrincipal; }; @@ -299,11 +314,18 @@ export interface HttpAuthService { req: Request_2, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise>; // (undocumented) - issueUserCookie(res: Response_2): Promise; + issueUserCookie( + res: Response_2, + options?: { + credentials?: BackstageCredentials; + }, + ): Promise<{ + expiresAt: Date; + }>; } // @public (undocumented) diff --git a/packages/backend-plugin-api/src/services/definitions/AuthService.ts b/packages/backend-plugin-api/src/services/definitions/AuthService.ts index eab0a7fac7..2bcdc975a0 100644 --- a/packages/backend-plugin-api/src/services/definitions/AuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/AuthService.ts @@ -46,6 +46,8 @@ export type BackstageServicePrincipal = { export type BackstageCredentials = { $$type: '@backstage/BackstageCredentials'; + expiresAt?: Date; + principal: TPrincipal; }; @@ -63,13 +65,20 @@ export type BackstagePrincipalTypes = { * @public */ export interface AuthService { - authenticate(token: string): Promise; + authenticate( + token: string, + options?: { + allowLimitedAccess?: boolean; + }, + ): Promise; isPrincipal( credentials: BackstageCredentials, type: TType, ): credentials is BackstageCredentials; + getNoneCredentials(): Promise>; + getOwnServiceCredentials(): Promise< BackstageCredentials >; @@ -78,4 +87,8 @@ export interface AuthService { onBehalfOf: BackstageCredentials; targetPluginId: string; }): Promise<{ token: string }>; + + getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ token: string; expiresAt: Date }>; } diff --git a/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts b/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts index 44696bd777..637109d1f0 100644 --- a/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts @@ -15,7 +15,11 @@ */ import { Request, Response } from 'express'; -import { BackstageCredentials, BackstagePrincipalTypes } from './AuthService'; +import { + BackstageCredentials, + BackstagePrincipalTypes, + BackstageUserPrincipal, +} from './AuthService'; /** @public */ export interface HttpAuthService { @@ -23,9 +27,14 @@ export interface HttpAuthService { req: Request, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise>; - issueUserCookie(res: Response): Promise; + issueUserCookie( + res: Response, + options?: { + credentials?: BackstageCredentials; + }, + ): Promise<{ expiresAt: Date }>; } diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 6a8b9b4fc3..9f6743fe37 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -42,7 +42,7 @@ "knex": "^3.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "zod": "^3.22.4" }, diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index a9ebb402c4..ff3f50ef8c 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -12,6 +12,7 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { BackstageNonePrincipal } from '@backstage/backend-plugin-api'; import { BackstageServicePrincipal } from '@backstage/backend-plugin-api'; +import { BackstageUserInfo } from '@backstage/backend-plugin-api'; import { BackstageUserPrincipal } from '@backstage/backend-plugin-api'; import { CacheService } from '@backstage/backend-plugin-api'; import { DatabaseService } from '@backstage/backend-plugin-api'; @@ -37,6 +38,7 @@ import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { UrlReaderService } from '@backstage/backend-plugin-api'; +import { UserInfoService } from '@backstage/backend-plugin-api'; // @public export function createMockDirectory( @@ -48,6 +50,17 @@ export function isDockerDisabledForTests(): boolean; // @public (undocumented) export namespace mockCredentials { + export function limitedUser( + userEntityRef?: string, + ): BackstageCredentials; + export namespace limitedUser { + export function cookie(userEntityRef?: string): string; + // (undocumented) + export function invalidCookie(): string; + // (undocumented) + export function invalidToken(): string; + export function token(userEntityRef?: string): string; + } export function none(): BackstageCredentials; export namespace none { export function header(): string; @@ -316,6 +329,17 @@ export namespace mockServices { partialImpl?: Partial | undefined, ) => ServiceMock; } + export function userInfo( + customInfo?: Partial, + ): UserInfoService; + // (undocumented) + export namespace userInfo { + const factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; + } } // @public diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 4153fba211..801b7c125b 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,16 +1,30 @@ { "name": "@backstage/backend-test-utils", - "description": "Test helpers library for Backstage backends", "version": "0.3.0", - "main": "src/index.ts", - "types": "src/index.ts", + "description": "Test helpers library for Backstage backends", + "backstage": { + "role": "node-library" + }, "publishConfig": { "access": "public" }, + "keywords": [ + "backstage", + "test" + ], + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend-test-utils" + }, + "license": "Apache-2.0", "exports": { ".": "./src/index.ts", "./package.json": "./package.json" }, + "main": "src/index.ts", + "types": "src/index.ts", "typesVersions": { "*": { "package.json": [ @@ -18,28 +32,17 @@ ] } }, - "backstage": { - "role": "node-library" - }, - "homepage": "https://backstage.io", - "repository": { - "type": "git", - "url": "https://github.com/backstage/backstage", - "directory": "packages/backend-test-utils" - }, - "keywords": [ - "backstage", - "test" + "files": [ + "dist" ], - "license": "Apache-2.0", "scripts": { "build": "backstage-cli package build", + "clean": "backstage-cli package clean", "lint": "backstage-cli package lint", - "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", - "clean": "backstage-cli package clean", - "start": "backstage-cli package start" + "start": "backstage-cli package start", + "test": "backstage-cli package test" }, "dependencies": { "@backstage/backend-app-api": "workspace:^", @@ -50,25 +53,23 @@ "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", "better-sqlite3": "^9.0.0", + "cookie": "^0.6.0", "express": "^4.17.1", "fs-extra": "^11.0.0", "knex": "^3.0.0", "msw": "^1.0.0", "mysql2": "^3.0.0", "pg": "^8.11.3", - "testcontainers": "^8.1.2", + "testcontainers": "^10.0.0", "textextensions": "^5.16.0", - "uuid": "^8.0.0" - }, - "peerDependencies": { - "@types/jest": "*" + "uuid": "^9.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, - "files": [ - "dist" - ] + "peerDependencies": { + "@types/jest": "*" + } } diff --git a/packages/backend-test-utils/src/database/startMysqlContainer.ts b/packages/backend-test-utils/src/database/startMysqlContainer.ts index fa99389cbc..2b4c917393 100644 --- a/packages/backend-test-utils/src/database/startMysqlContainer.ts +++ b/packages/backend-test-utils/src/database/startMysqlContainer.ts @@ -54,7 +54,7 @@ export async function startMysqlContainer(image: string) { const container = await new GenericContainer(image) .withExposedPorts(3306) - .withEnv('MYSQL_ROOT_PASSWORD', password) + .withEnvironment({ MYSQL_ROOT_PASSWORD: password }) .withTmpFs({ '/var/lib/mysql': 'rw' }) .start(); diff --git a/packages/backend-test-utils/src/database/startPostgresContainer.ts b/packages/backend-test-utils/src/database/startPostgresContainer.ts index 81358e01d3..7a1c3f89c5 100644 --- a/packages/backend-test-utils/src/database/startPostgresContainer.ts +++ b/packages/backend-test-utils/src/database/startPostgresContainer.ts @@ -54,7 +54,7 @@ export async function startPostgresContainer(image: string) { const container = await new GenericContainer(image) .withExposedPorts(5432) - .withEnv('POSTGRES_PASSWORD', password) + .withEnvironment({ POSTGRES_PASSWORD: password }) .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) .start(); diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.test.ts b/packages/backend-test-utils/src/next/services/MockAuthService.test.ts index dfb54b9762..8741220a68 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.test.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.test.ts @@ -53,6 +53,12 @@ describe('MockAuthService', () => { auth.authenticate(mockCredentials.user.token()), ).resolves.toEqual(mockCredentials.user()); + await expect( + auth.authenticate(mockCredentials.user.token(), { + allowLimitedAccess: true, + }), + ).resolves.toEqual(mockCredentials.user()); + await expect( auth.authenticate(mockCredentials.user.token()), ).resolves.toEqual(mockCredentials.user(DEFAULT_MOCK_USER_ENTITY_REF)); @@ -66,6 +72,44 @@ describe('MockAuthService', () => { ).rejects.toThrow('User token is invalid'); }); + it('should authenticate mock limited user tokens', async () => { + await expect( + auth.authenticate(mockCredentials.limitedUser.token()), + ).rejects.toThrow('Limited user token is not allowed'); + await expect( + auth.authenticate(mockCredentials.limitedUser.token(), {}), + ).rejects.toThrow('Limited user token is not allowed'); + await expect( + auth.authenticate(mockCredentials.limitedUser.token(), { + allowLimitedAccess: false, + }), + ).rejects.toThrow('Limited user token is not allowed'); + await expect( + auth.authenticate(mockCredentials.limitedUser.token(), { + allowLimitedAccess: true, + }), + ).resolves.toEqual(mockCredentials.user()); + + await expect( + auth.authenticate(mockCredentials.limitedUser.token(), { + allowLimitedAccess: true, + }), + ).resolves.toEqual(mockCredentials.user(DEFAULT_MOCK_USER_ENTITY_REF)); + + await expect( + auth.authenticate( + mockCredentials.limitedUser.token('user:default/other'), + { + allowLimitedAccess: true, + }, + ), + ).resolves.toEqual(mockCredentials.user('user:default/other')); + + await expect( + auth.authenticate(mockCredentials.limitedUser.invalidToken()), + ).rejects.toThrow('Limited user token is invalid'); + }); + it('should authenticate mock service tokens', async () => { await expect( auth.authenticate(mockCredentials.service.token()), @@ -113,6 +157,12 @@ describe('MockAuthService', () => { ).rejects.toThrow('Service token is invalid'); }); + it('should return none credentials', async () => { + await expect(auth.getNoneCredentials()).resolves.toEqual( + mockCredentials.none(), + ); + }); + it('should return own service credentials', async () => { await expect(auth.getOwnServiceCredentials()).resolves.toEqual( mockCredentials.service('plugin:test'), @@ -211,4 +261,32 @@ describe('MockAuthService', () => { `Refused to issue service token for credential type 'none'`, ); }); + + it('should issue limited user tokens', async () => { + await expect( + auth.getLimitedUserToken(mockCredentials.user()), + ).resolves.toEqual({ + token: mockCredentials.limitedUser.token(), + expiresAt: expect.any(Date), + }); + + await expect( + auth.getLimitedUserToken(mockCredentials.user('user:default/other')), + ).resolves.toEqual({ + token: mockCredentials.limitedUser.token('user:default/other'), + expiresAt: expect.any(Date), + }); + + await expect( + auth.getLimitedUserToken(mockCredentials.none() as any), + ).rejects.toThrow( + "Refused to issue limited user token for credential type 'none'", + ); + + await expect( + auth.getLimitedUserToken(mockCredentials.service() as any), + ).rejects.toThrow( + "Refused to issue limited user token for credential type 'service'", + ); + }); }); diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.ts b/packages/backend-test-utils/src/next/services/MockAuthService.ts index 6a18711798..64dc92747a 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.ts @@ -27,9 +27,11 @@ import { mockCredentials, MOCK_USER_TOKEN, MOCK_USER_TOKEN_PREFIX, + MOCK_INVALID_USER_TOKEN, + MOCK_USER_LIMITED_TOKEN_PREFIX, + MOCK_INVALID_USER_LIMITED_TOKEN, MOCK_SERVICE_TOKEN, MOCK_SERVICE_TOKEN_PREFIX, - MOCK_INVALID_USER_TOKEN, MOCK_INVALID_SERVICE_TOKEN, UserTokenPayload, ServiceTokenPayload, @@ -48,7 +50,10 @@ export class MockAuthService implements AuthService { this.disableDefaultAuthPolicy = options.disableDefaultAuthPolicy; } - async authenticate(token: string): Promise { + async authenticate( + token: string, + options?: { allowLimitedAccess?: boolean }, + ): Promise { switch (token) { case MOCK_USER_TOKEN: return mockCredentials.user(); @@ -56,6 +61,8 @@ export class MockAuthService implements AuthService { return mockCredentials.service(); case MOCK_INVALID_USER_TOKEN: throw new AuthenticationError('User token is invalid'); + case MOCK_INVALID_USER_LIMITED_TOKEN: + throw new AuthenticationError('Limited user token is invalid'); case MOCK_INVALID_SERVICE_TOKEN: throw new AuthenticationError('Service token is invalid'); case '': @@ -72,6 +79,18 @@ export class MockAuthService implements AuthService { return mockCredentials.user(userEntityRef); } + if (token.startsWith(MOCK_USER_LIMITED_TOKEN_PREFIX)) { + if (!options?.allowLimitedAccess) { + throw new AuthenticationError('Limited user token is not allowed'); + } + + const { sub: userEntityRef }: UserTokenPayload = JSON.parse( + token.slice(MOCK_USER_LIMITED_TOKEN_PREFIX.length), + ); + + return mockCredentials.user(userEntityRef); + } + if (token.startsWith(MOCK_SERVICE_TOKEN_PREFIX)) { const { sub, target, obo }: ServiceTokenPayload = JSON.parse( token.slice(MOCK_SERVICE_TOKEN_PREFIX.length), @@ -92,6 +111,10 @@ export class MockAuthService implements AuthService { throw new AuthenticationError(`Unknown mock token '${token}'`); } + async getNoneCredentials() { + return mockCredentials.none(); + } + async getOwnServiceCredentials(): Promise< BackstageCredentials > { @@ -144,4 +167,21 @@ export class MockAuthService implements AuthService { }), }; } + + async getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ token: string; expiresAt: Date }> { + if (credentials.principal.type !== 'user') { + throw new AuthenticationError( + `Refused to issue limited user token for credential type '${credentials.principal.type}'`, + ); + } + + return { + token: mockCredentials.limitedUser.token( + credentials.principal.userEntityRef, + ), + expiresAt: new Date(Date.now() + 3600_000), + }; + } } diff --git a/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts b/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts index 922b3daaf4..b43f448b44 100644 --- a/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts +++ b/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts @@ -22,8 +22,11 @@ import { AuthenticationError } from '@backstage/errors'; describe('MockHttpAuthService', () => { const httpAuth = new MockHttpAuthService('test', mockCredentials.none()); - function makeAuthReq(header?: string) { - return { headers: { authorization: header } } as Request; + function makeAuthReq(authorization?: string) { + return { headers: { authorization } } as Request; + } + function makeCookieAuthReq(cookie?: string) { + return { headers: { cookie } } as Request; } it('should authenticate unauthenticated requests', async () => { @@ -68,6 +71,59 @@ describe('MockHttpAuthService', () => { ).resolves.toEqual(mockCredentials.user('user:default/other')); }); + it('should authenticate limited user requests', async () => { + await expect( + httpAuth.credentials( + makeCookieAuthReq(mockCredentials.limitedUser.cookie()), + ), + ).resolves.toEqual(mockCredentials.none()); + + await expect( + httpAuth.credentials( + makeCookieAuthReq(mockCredentials.limitedUser.cookie()), + { allowLimitedAccess: true }, + ), + ).resolves.toEqual(mockCredentials.user()); + + await expect( + httpAuth.credentials(makeAuthReq(mockCredentials.user.header()), { + allowLimitedAccess: true, + }), + ).resolves.toEqual(mockCredentials.user()); + + await expect( + httpAuth.credentials( + makeCookieAuthReq(mockCredentials.limitedUser.cookie()), + { + allow: ['user'], + }, + ), + ).rejects.toThrow('Missing credentials'); + + await expect( + httpAuth.credentials( + makeCookieAuthReq(mockCredentials.limitedUser.cookie()), + { + allow: ['none', 'service'], + allowLimitedAccess: true, + }, + ), + ).rejects.toThrow("This endpoint does not allow 'user' credentials"); + + await expect( + httpAuth.credentials( + makeAuthReq(`Bearer ${mockCredentials.limitedUser.token()}`), + { allowLimitedAccess: true }, + ), + ).resolves.toEqual(mockCredentials.user()); + + await expect( + httpAuth.credentials( + makeAuthReq(`Bearer ${mockCredentials.limitedUser.token()}`), + ), + ).rejects.toThrow('Limited user token is not allowed'); + }); + it('should authenticate service requests', async () => { await expect( httpAuth.credentials(makeAuthReq(mockCredentials.service.header())), @@ -161,9 +217,42 @@ describe('MockHttpAuthService', () => { ).rejects.toThrow('Service token is invalid'); }); - it('does not implement .issueUserCookie', async () => { - await expect(httpAuth.issueUserCookie({} as any)).rejects.toThrow( - 'Not implemented', + it('should issue user cookie from request credentials', async () => { + const setHeader = jest.fn(); + + await expect( + httpAuth.issueUserCookie({ + req: makeAuthReq(mockCredentials.user.header()), + setHeader, + } as any), + ).resolves.toEqual({ + expiresAt: expect.any(Date), + }); + + expect(setHeader).toHaveBeenCalledWith( + 'Set-Cookie', + mockCredentials.limitedUser.cookie(), + ); + }); + + it('should issue user cookie from explicit credentials', async () => { + const setHeader = jest.fn(); + + await expect( + httpAuth.issueUserCookie( + { + req: makeAuthReq(mockCredentials.user.header()), + setHeader, + } as any, + { credentials: mockCredentials.user('user:default/other') }, + ), + ).resolves.toEqual({ + expiresAt: expect.any(Date), + }); + + expect(setHeader).toHaveBeenCalledWith( + 'Set-Cookie', + mockCredentials.limitedUser.cookie('user:default/other'), ); }); }); diff --git a/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts b/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts index 9b133f996b..9a69620473 100644 --- a/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts @@ -18,16 +18,18 @@ import { AuthService, BackstageCredentials, BackstagePrincipalTypes, + BackstageUserPrincipal, HttpAuthService, } from '@backstage/backend-plugin-api'; import { Request, Response } from 'express'; +import { parse as parseCookie } from 'cookie'; import { MockAuthService } from './MockAuthService'; +import { AuthenticationError, NotAllowedError } from '@backstage/errors'; import { - AuthenticationError, - NotAllowedError, - NotImplementedError, -} from '@backstage/errors'; -import { mockCredentials } from './mockCredentials'; + MOCK_NONE_TOKEN, + MOCK_AUTH_COOKIE, + mockCredentials, +} from './mockCredentials'; // TODO: support mock cookie auth? export class MockHttpAuthService implements HttpAuthService { @@ -42,33 +44,52 @@ export class MockHttpAuthService implements HttpAuthService { this.#defaultCredentials = defaultCredentials; } - async #getCredentials(req: Request) { + async #getCredentials(req: Request, allowLimitedAccess: boolean) { const header = req.headers.authorization; - - if (header === mockCredentials.none.header()) { - return mockCredentials.none(); - } - const token = typeof header === 'string' ? header.match(/^Bearer[ ]+(\S+)$/i)?.[1] : undefined; - if (!token) { - return this.#defaultCredentials; + if (token) { + if (token === MOCK_NONE_TOKEN) { + return this.#auth.getNoneCredentials(); + } + + return await this.#auth.authenticate(token, { + allowLimitedAccess, + }); } - return await this.#auth.authenticate(token); + if (allowLimitedAccess) { + const cookieHeader = req.headers.cookie; + + if (cookieHeader) { + const cookies = parseCookie(cookieHeader); + const cookie = cookies[MOCK_AUTH_COOKIE]; + + if (cookie) { + return await this.#auth.authenticate(cookie, { + allowLimitedAccess: true, + }); + } + } + } + + return this.#defaultCredentials; } async credentials( req: Request, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise> { - const credentials = await this.#getCredentials(req); + const credentials = await this.#getCredentials( + req, + options?.allowLimitedAccess ?? false, + ); const allowedPrincipalTypes = options?.allow; if (!allowedPrincipalTypes) { @@ -80,7 +101,7 @@ export class MockHttpAuthService implements HttpAuthService { return credentials as any; } - throw new AuthenticationError(); + throw new AuthenticationError('Missing credentials'); } else if (this.#auth.isPrincipal(credentials, 'user')) { if (allowedPrincipalTypes.includes('user' as TAllowed)) { return credentials as any; @@ -104,7 +125,19 @@ export class MockHttpAuthService implements HttpAuthService { ); } - async issueUserCookie(_res: Response): Promise { - throw new NotImplementedError('Not implemented'); + async issueUserCookie( + res: Response, + options?: { credentials?: BackstageCredentials }, + ): Promise<{ expiresAt: Date }> { + const credentials = + options?.credentials ?? + (await this.credentials(res.req, { allow: ['user'] })); + + res.setHeader( + 'Set-Cookie', + mockCredentials.limitedUser.cookie(credentials.principal.userEntityRef), + ); + + return { expiresAt: new Date(Date.now() + 3600_000) }; } } diff --git a/packages/backend-test-utils/src/next/services/MockUserInfoService.test.ts b/packages/backend-test-utils/src/next/services/MockUserInfoService.test.ts new file mode 100644 index 0000000000..13c8a43213 --- /dev/null +++ b/packages/backend-test-utils/src/next/services/MockUserInfoService.test.ts @@ -0,0 +1,55 @@ +/* + * 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 { MockUserInfoService } from './MockUserInfoService'; +import { mockCredentials } from './mockCredentials'; + +describe('MockUserInfoService', () => { + it('works without constructor parameters', async () => { + const service = new MockUserInfoService(); + const user = mockCredentials.user(); + await expect(service.getUserInfo(user)).resolves.toEqual({ + userEntityRef: user.principal.userEntityRef, + ownershipEntityRefs: [user.principal.userEntityRef], + }); + }); + + it('works with custom constructor parameters', async () => { + const service = new MockUserInfoService({ + userEntityRef: 'user:default/not-the-mock-1', + ownershipEntityRefs: ['user:default/not-the-mock-2'], + }); + const user = mockCredentials.user(); + await expect(service.getUserInfo(user)).resolves.toEqual({ + userEntityRef: 'user:default/not-the-mock-1', + ownershipEntityRefs: ['user:default/not-the-mock-2'], + }); + }); + + it('rejects non-users', async () => { + const service = new MockUserInfoService(); + await expect( + service.getUserInfo(mockCredentials.none()), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"User info not available for principal type 'none'"`, + ); + await expect( + service.getUserInfo(mockCredentials.service()), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"User info not available for principal type 'service'"`, + ); + }); +}); diff --git a/packages/backend-test-utils/src/next/services/MockUserInfoService.ts b/packages/backend-test-utils/src/next/services/MockUserInfoService.ts new file mode 100644 index 0000000000..68c2a8acae --- /dev/null +++ b/packages/backend-test-utils/src/next/services/MockUserInfoService.ts @@ -0,0 +1,55 @@ +/* + * 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 { + BackstageCredentials, + BackstageNonePrincipal, + BackstageServicePrincipal, + BackstageUserInfo, + BackstageUserPrincipal, + UserInfoService, +} from '@backstage/backend-plugin-api'; +import { InputError } from '@backstage/errors'; + +/** @internal */ +export class MockUserInfoService implements UserInfoService { + private readonly customInfo: Partial; + + constructor(customInfo?: Partial) { + this.customInfo = customInfo ?? {}; + } + + async getUserInfo( + credentials: BackstageCredentials, + ): Promise { + const principal = credentials.principal as + | BackstageUserPrincipal + | BackstageServicePrincipal + | BackstageNonePrincipal; + + if (principal.type !== 'user') { + throw new InputError( + `User info not available for principal type '${principal.type}'`, + ); + } + + return { + userEntityRef: principal.userEntityRef, + ownershipEntityRefs: [principal.userEntityRef], + ...this.customInfo, + }; + } +} diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts index 67cda92be8..ed0071d4b8 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts @@ -36,6 +36,18 @@ describe('mockCredentials', () => { }); }); + it('creates a mocked credentials object for a limited user principal', () => { + expect(mockCredentials.limitedUser()).toEqual({ + $$type: '@backstage/BackstageCredentials', + principal: { type: 'user', userEntityRef: 'user:default/mock' }, + }); + + expect(mockCredentials.limitedUser('user:default/other')).toEqual({ + $$type: '@backstage/BackstageCredentials', + principal: { type: 'user', userEntityRef: 'user:default/other' }, + }); + }); + it('creates a mocked credentials object for a service principal', () => { expect(mockCredentials.service()).toEqual({ $$type: '@backstage/BackstageCredentials', @@ -68,6 +80,22 @@ describe('mockCredentials', () => { ); }); + it('creates limited user tokens and headers', () => { + expect(mockCredentials.limitedUser.token('user:default/other')).toBe( + 'mock-limited-user-token:{"sub":"user:default/other"}', + ); + expect(mockCredentials.limitedUser.invalidToken()).toBe( + 'mock-invalid-limited-user-token', + ); + + expect(mockCredentials.limitedUser.cookie('user:default/other')).toBe( + 'backstage-auth=mock-limited-user-token:{"sub":"user:default/other"}', + ); + expect(mockCredentials.limitedUser.invalidCookie()).toBe( + 'backstage-auth=mock-invalid-limited-user-token', + ); + }); + it('creates service tokens and headers', () => { expect(mockCredentials.service.token()).toBe('mock-service-token'); expect( diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.ts b/packages/backend-test-utils/src/next/services/mockCredentials.ts index 8dac9c1a4b..16d2381c73 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.ts @@ -24,10 +24,18 @@ import { export const DEFAULT_MOCK_USER_ENTITY_REF = 'user:default/mock'; export const DEFAULT_MOCK_SERVICE_SUBJECT = 'external:test-service'; +export const MOCK_AUTH_COOKIE = 'backstage-auth'; + export const MOCK_NONE_TOKEN = 'mock-none-token'; + export const MOCK_USER_TOKEN = 'mock-user-token'; export const MOCK_USER_TOKEN_PREFIX = 'mock-user-token:'; export const MOCK_INVALID_USER_TOKEN = 'mock-invalid-user-token'; + +export const MOCK_USER_LIMITED_TOKEN_PREFIX = 'mock-limited-user-token:'; +export const MOCK_INVALID_USER_LIMITED_TOKEN = + 'mock-invalid-limited-user-token'; + export const MOCK_SERVICE_TOKEN = 'mock-service-token'; export const MOCK_SERVICE_TOKEN_PREFIX = 'mock-service-token:'; export const MOCK_INVALID_SERVICE_TOKEN = 'mock-invalid-service-token'; @@ -143,6 +151,54 @@ export namespace mockCredentials { } } + /** + * Creates a mocked credentials object for a user principal with limited + * access. + * + * The default user entity reference is 'user:default/mock'. + */ + export function limitedUser( + userEntityRef: string = DEFAULT_MOCK_USER_ENTITY_REF, + ): BackstageCredentials { + return user(userEntityRef); + } + + /** + * Utilities related to limited user credentials. + */ + export namespace limitedUser { + /** + * Creates a mocked limited user token. If a payload is provided it will be + * encoded into the token and forwarded to the credentials object when + * authenticated by the mock auth service. + */ + export function token( + userEntityRef: string = DEFAULT_MOCK_USER_ENTITY_REF, + ): string { + validateUserEntityRef(userEntityRef); + return `${MOCK_USER_LIMITED_TOKEN_PREFIX}${JSON.stringify({ + sub: userEntityRef, + } satisfies UserTokenPayload)}`; + } + + /** + * Returns an authorization header with a mocked limited user token. If a + * payload is provided it will be encoded into the token and forwarded to + * the credentials object when authenticated by the mock auth service. + */ + export function cookie(userEntityRef?: string): string { + return `${MOCK_AUTH_COOKIE}=${token(userEntityRef)}`; + } + + export function invalidToken(): string { + return MOCK_INVALID_USER_LIMITED_TOKEN; + } + + export function invalidCookie(): string { + return `${MOCK_AUTH_COOKIE}=${invalidToken()}`; + } + } + /** * Creates a mocked credentials object for a service principal. * diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index f72f229434..3ff8a4e017 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -27,6 +27,8 @@ import { DiscoveryService, HttpAuthService, BackstageCredentials, + BackstageUserInfo, + UserInfoService, } from '@backstage/backend-plugin-api'; import { cacheServiceFactory, @@ -49,6 +51,7 @@ import { MockRootLoggerService } from './MockRootLoggerService'; import { MockAuthService } from './MockAuthService'; import { MockHttpAuthService } from './MockHttpAuthService'; import { mockCredentials } from './mockCredentials'; +import { MockUserInfoService } from './MockUserInfoService'; /** @internal */ function simpleFactory< @@ -199,9 +202,11 @@ export namespace mockServices { }); export const mock = simpleMock(coreServices.auth, () => ({ authenticate: jest.fn(), + getNoneCredentials: jest.fn(), getOwnServiceCredentials: jest.fn(), isPrincipal: jest.fn() as any, getPluginRequestToken: jest.fn(), + getLimitedUserToken: jest.fn(), })); } @@ -272,6 +277,37 @@ export namespace mockServices { })); } + /** + * Creates a mock implementation of the `UserInfoService`. + * + * By default it extracts the user's entity ref from a user principal and + * returns that as the only ownership entity ref, but this can be overridden + * by passing in a custom set of user info. + */ + export function userInfo( + customInfo?: Partial, + ): UserInfoService { + return new MockUserInfoService(customInfo); + } + export namespace userInfo { + /** + * Creates a mock service factory for the `UserInfoService`. + * + * By default it extracts the user's entity ref from a user principal and + * returns that as the only ownership entity ref. + */ + export const factory = createServiceFactory({ + service: coreServices.userInfo, + deps: {}, + factory() { + return new MockUserInfoService(); + }, + }); + export const mock = simpleMock(coreServices.userInfo, () => ({ + getUserInfo: jest.fn(), + })); + } + // TODO(Rugvip): Not all core services have implementations available here yet. // some may need a bit more refactoring for it to be simpler to // re-implement functioning mock versions here. @@ -284,12 +320,14 @@ export namespace mockServices { withOptions: jest.fn(), })); } + export namespace database { export const factory = databaseServiceFactory; export const mock = simpleMock(coreServices.database, () => ({ getClient: jest.fn(), })); } + export namespace httpRouter { export const factory = httpRouterServiceFactory; export const mock = simpleMock(coreServices.httpRouter, () => ({ @@ -297,12 +335,14 @@ export namespace mockServices { addAuthPolicy: jest.fn(), })); } + export namespace rootHttpRouter { export const factory = rootHttpRouterServiceFactory; export const mock = simpleMock(coreServices.rootHttpRouter, () => ({ use: jest.fn(), })); } + export namespace lifecycle { export const factory = lifecycleServiceFactory; export const mock = simpleMock(coreServices.lifecycle, () => ({ @@ -310,6 +350,7 @@ export namespace mockServices { addStartupHook: jest.fn(), })); } + export namespace logger { export const factory = loggerServiceFactory; export const mock = simpleMock(coreServices.logger, () => ({ @@ -320,6 +361,7 @@ export namespace mockServices { warn: jest.fn(), })); } + export namespace permissions { export const factory = permissionsServiceFactory; export const mock = simpleMock(coreServices.permissions, () => ({ @@ -327,6 +369,7 @@ export namespace mockServices { authorizeConditional: jest.fn(), })); } + export namespace rootLifecycle { export const factory = rootLifecycleServiceFactory; export const mock = simpleMock(coreServices.rootLifecycle, () => ({ @@ -334,6 +377,7 @@ export namespace mockServices { addStartupHook: jest.fn(), })); } + export namespace scheduler { export const factory = schedulerServiceFactory; export const mock = simpleMock(coreServices.scheduler, () => ({ @@ -343,6 +387,7 @@ export namespace mockServices { triggerTask: jest.fn(), })); } + export namespace urlReader { export const factory = urlReaderServiceFactory; export const mock = simpleMock(coreServices.urlReader, () => ({ diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 72a2ed8edb..6b58ebc5ea 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -80,6 +80,7 @@ export const defaultServiceFactories = [ mockServices.rootLogger.factory(), mockServices.scheduler.factory(), mockServices.tokenManager.factory(), + mockServices.userInfo.factory(), mockServices.urlReader.factory(), ]; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index bade028597..8931e6832b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -70,6 +70,7 @@ import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import { DefaultEventBroker } from '@backstage/plugin-events-backend'; +import { DefaultEventsService } from '@backstage/plugin-events-node'; import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'; import { MeterProvider } from '@opentelemetry/sdk-metrics'; import { metrics } from '@opentelemetry/api'; @@ -99,7 +100,11 @@ function makeCreateEnv(config: Config) { discovery, }); - const eventBroker = new DefaultEventBroker(root.child({ type: 'plugin' })); + const eventsService = DefaultEventsService.create({ logger: root }); + const eventBroker = new DefaultEventBroker( + root.child({ type: 'plugin' }), + eventsService, + ); const signalService = DefaultSignalService.create({ eventBroker, }); @@ -119,6 +124,7 @@ function makeCreateEnv(config: Config) { config, reader, eventBroker, + events: eventsService, discovery, tokenManager, permissions, diff --git a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts index 7a032198a3..11d073400a 100644 --- a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts +++ b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts @@ -18,40 +18,36 @@ import { EntityProvider, EntityProviderConnection, } from '@backstage/plugin-catalog-node'; -import { - EventBroker, - EventParams, - EventSubscriber, -} from '@backstage/plugin-events-node'; +import { EventParams, EventsService } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; -export class DemoEventBasedEntityProvider - implements EntityProvider, EventSubscriber -{ +export class DemoEventBasedEntityProvider implements EntityProvider { private readonly logger: Logger; + private readonly events: EventsService; private readonly topics: string[]; constructor(opts: { - eventBroker: EventBroker; + events: EventsService; logger: Logger; topics: string[]; }) { - const { eventBroker, logger, topics } = opts; - this.logger = logger; - this.topics = topics; - eventBroker.subscribe(this); + this.events = opts.events; + this.logger = opts.logger; + this.topics = opts.topics; } - async onEvent(params: EventParams): Promise { - this.logger.info( - `onEvent: topic=${params.topic}, metadata=${JSON.stringify( - params.metadata, - )}, payload=${JSON.stringify(params.eventPayload)}`, - ); - } - - supportsEventTopics(): string[] { - return this.topics; + async subscribe() { + await this.events.subscribe({ + id: 'DemoEventBasedEntityProvider', + topics: this.topics, + onEvent: async (params: EventParams): Promise => { + this.logger.info( + `onEvent: topic=${params.topic}, metadata=${JSON.stringify( + params.metadata, + )}, payload=${JSON.stringify(params.eventPayload)}`, + ); + }, + }); } async connect(_: EntityProviderConnection): Promise { diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 223acab818..00fe7ff4a0 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -28,10 +28,11 @@ export default async function createPlugin( builder.addProcessor(new ScaffolderEntitiesProcessor()); const demoProvider = new DemoEventBasedEntityProvider({ + events: env.events, logger: env.logger, topics: ['example'], - eventBroker: env.eventBroker, }); + await demoProvider.subscribe(); builder.addEntityProvider(demoProvider); const { processingEngine, router } = await builder.build(); diff --git a/packages/backend/src/plugins/events.ts b/packages/backend/src/plugins/events.ts index f3ff354240..fd60a9bb14 100644 --- a/packages/backend/src/plugins/events.ts +++ b/packages/backend/src/plugins/events.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - EventsBackend, - HttpPostIngressEventPublisher, -} from '@backstage/plugin-events-backend'; +import { HttpPostIngressEventPublisher } from '@backstage/plugin-events-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -28,14 +25,10 @@ export default async function createPlugin( const http = HttpPostIngressEventPublisher.fromConfig({ config: env.config, + events: env.events, logger: env.logger, }); http.bind(eventsRouter); - await new EventsBackend(env.logger) - .setEventBroker(env.eventBroker) - .addPublishers(http) - .start(); - return eventsRouter; } diff --git a/packages/backend/src/plugins/jenkins.ts b/packages/backend/src/plugins/jenkins.ts index d62200b0ac..7d47ee338d 100644 --- a/packages/backend/src/plugins/jenkins.ts +++ b/packages/backend/src/plugins/jenkins.ts @@ -32,6 +32,8 @@ export default async function createPlugin( jenkinsInfoProvider: DefaultJenkinsInfoProvider.fromConfig({ catalog, config: env.config, + discovery: env.discovery, }), + discovery: env.discovery, }); } diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts index 3bc6648862..5581083879 100644 --- a/packages/backend/src/plugins/kubernetes.ts +++ b/packages/backend/src/plugins/kubernetes.ts @@ -28,6 +28,7 @@ export default async function createPlugin( config: env.config, catalogApi, permissions: env.permissions, + discovery: env.discovery, }).build(); return router; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index d76e68c1c9..7d9cd19310 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -26,7 +26,7 @@ import { import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; -import { EventBroker } from '@backstage/plugin-events-node'; +import { EventBroker, EventsService } from '@backstage/plugin-events-node'; import { SignalService } from '@backstage/plugin-signals-node'; export type PluginEnvironment = { @@ -40,6 +40,10 @@ export type PluginEnvironment = { permissions: PermissionEvaluator; scheduler: PluginTaskScheduler; identity: IdentityApi; + /** + * @deprecated use `events` instead + */ eventBroker: EventBroker; + events: EventsService; signalService: SignalService; }; diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index c36b3d96b5..81c0a6af5c 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -761,7 +761,7 @@ describe('CatalogClient', () => { }, 'url:http://example.com', ), - ).rejects.toThrow(/Request failed with 500 Error/); + ).rejects.toThrow(/Request failed with 500 Internal Server Error/); }); }); }); diff --git a/packages/catalog-model/examples/acme/team-a-group.yaml b/packages/catalog-model/examples/acme/team-a-group.yaml index 7fe0e7b3f3..eb95c47093 100644 --- a/packages/catalog-model/examples/acme/team-a-group.yaml +++ b/packages/catalog-model/examples/acme/team-a-group.yaml @@ -57,3 +57,17 @@ spec: displayName: Guest User email: guest@example.com memberOf: [team-a] +--- +# This user is added as an example, to make it more easy for the "Guest" +# sign-in option to demonstrate some entities being owned. In a regular org, +# a guest user would probably not be registered like this. +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: guest + namespace: development +spec: + profile: + displayName: Guest User + email: guest@example.com + memberOf: [group:default/team-a] diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 27b9c62653..7641386176 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -66,6 +66,15 @@ export type AlertDisplayProps = { transientTimeoutMs?: number; }; +// @public +export function AppIcon(props: AppIconProps): React_2.JSX.Element; + +// @public +export type AppIconProps = IconComponentProps & { + id: string; + Fallback?: IconComponent; +}; + // @public export const AutoLogout: (props: AutoLogoutProps) => JSX.Element | null; @@ -130,8 +139,6 @@ export type BreadcrumbsClickableTextClassKey = 'root'; // @public (undocumented) export type BreadcrumbsStyledBoxClassKey = 'root'; -// Warning: (ae-forgotten-export) The symbol "IconComponentProps" needs to be exported by the entry point index.d.ts -// // @public export function BrokenImageIcon(props: IconComponentProps): React_2.JSX.Element; @@ -541,6 +548,9 @@ export type HorizontalScrollGridClassKey = | 'buttonLeft' | 'buttonRight'; +// @public +export type IconComponentProps = ComponentProps; + // @public (undocumented) export function IconLinkVertical({ color, diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx index ec7f87eb94..b1a2139b85 100644 --- a/packages/core-components/src/components/Select/Select.tsx +++ b/packages/core-components/src/components/Select/Select.tsx @@ -55,7 +55,6 @@ const BootstrapInput = withStyles( fontSize: theme.typography.body1.fontSize, padding: theme.spacing(1.25, 3.25, 1.25, 1.5), transition: theme.transitions.create(['border-color', 'box-shadow']), - fontFamily: 'Helvetica Neue', '&:focus': { background: theme.palette.background.paper, borderRadius: theme.shape.borderRadius, diff --git a/packages/core-components/src/components/Select/static/ClosedDropdown.tsx b/packages/core-components/src/components/Select/static/ClosedDropdown.tsx index 812afe34eb..00c748ce29 100644 --- a/packages/core-components/src/components/Select/static/ClosedDropdown.tsx +++ b/packages/core-components/src/components/Select/static/ClosedDropdown.tsx @@ -27,6 +27,7 @@ const useStyles = makeStyles( position: 'absolute', right: theme.spacing(0.5), pointerEvents: 'none', + color: '#616161', }, }), { name: 'BackstageClosedDropdown' }, @@ -42,7 +43,7 @@ const ClosedDropdown = () => { > ); diff --git a/packages/core-components/src/components/Select/static/OpenedDropdown.tsx b/packages/core-components/src/components/Select/static/OpenedDropdown.tsx index b87a00c26a..4ca2a5a065 100644 --- a/packages/core-components/src/components/Select/static/OpenedDropdown.tsx +++ b/packages/core-components/src/components/Select/static/OpenedDropdown.tsx @@ -26,6 +26,7 @@ const useStyles = makeStyles( position: 'absolute', right: theme.spacing(0.5), pointerEvents: 'none', + color: '#616161', }, }), { name: 'BackstageOpenedDropdown' }, @@ -41,7 +42,7 @@ const OpenedDropdown = () => { > ); diff --git a/packages/core-components/src/icons/icons.test.tsx b/packages/core-components/src/icons/icons.test.tsx new file mode 100644 index 0000000000..7cd54f37d0 --- /dev/null +++ b/packages/core-components/src/icons/icons.test.tsx @@ -0,0 +1,41 @@ +/* + * 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 React from 'react'; +import { screen } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { AppIcon } from './icons'; + +describe('AppIcon', () => { + it('should render the correct system icon', async () => { + await renderInTestApp(); + expect(screen.getByTestId('Api Icon')).toBeDefined(); + }); + + it('should render the default fallback component', async () => { + await renderInTestApp( + , + ); + expect(screen.getByTestId('Fallback Icon')).toBeDefined(); + }); + + it('should render the custom fallback component', async () => { + await renderInTestApp( +
Fallback Icon
} />, + ); + expect(screen.getByText('Fallback Icon')).toBeInTheDocument(); + }); +}); diff --git a/packages/core-components/src/icons/icons.tsx b/packages/core-components/src/icons/icons.tsx index 8d22af4664..ca3b6fa76a 100644 --- a/packages/core-components/src/icons/icons.tsx +++ b/packages/core-components/src/icons/icons.tsx @@ -18,61 +18,80 @@ import { IconComponent, useApp } from '@backstage/core-plugin-api'; import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; import React, { ComponentProps } from 'react'; -type IconComponentProps = ComponentProps; +/** + * @public + * Props for the {@link @backstage/core-plugin-api#IconComponent} component. + */ +export type IconComponentProps = ComponentProps; -function useSystemIcon(key: string, props: IconComponentProps) { +/** + * @public + * Props for the {@link AppIcon} component. + */ +export type AppIconProps = IconComponentProps & { + // The key of the system icon to render. + id: string; + // An optional fallback icon component to render when the system icon is not found. + // Default to () => null. + Fallback?: IconComponent; +}; + +/** + * @public + * A component that renders a system icon by its id. + */ +export function AppIcon(props: AppIconProps) { + const { id: key, Fallback = MuiBrokenImageIcon, ...rest } = props; const app = useApp(); - const Icon = app.getSystemIcon(key); - return Icon ? : ; + const Icon = app.getSystemIcon(key) ?? Fallback; + return ; } // Should match the list of overridable system icon keys in @backstage/core-app-api /** * Broken Image Icon - * * @public - * */ export function BrokenImageIcon(props: IconComponentProps) { - return useSystemIcon('brokenImage', props); + return ; } /** @public */ export function CatalogIcon(props: IconComponentProps) { - return useSystemIcon('catalog', props); + return ; } /** @public */ export function ChatIcon(props: IconComponentProps) { - return useSystemIcon('chat', props); + return ; } /** @public */ export function DashboardIcon(props: IconComponentProps) { - return useSystemIcon('dashboard', props); + return ; } /** @public */ export function DocsIcon(props: IconComponentProps) { - return useSystemIcon('docs', props); + return ; } /** @public */ export function EmailIcon(props: IconComponentProps) { - return useSystemIcon('email', props); + return ; } /** @public */ export function GitHubIcon(props: IconComponentProps) { - return useSystemIcon('github', props); + return ; } /** @public */ export function GroupIcon(props: IconComponentProps) { - return useSystemIcon('group', props); + return ; } /** @public */ export function HelpIcon(props: IconComponentProps) { - return useSystemIcon('help', props); + return ; } /** @public */ export function UserIcon(props: IconComponentProps) { - return useSystemIcon('user', props); + return ; } /** @public */ export function WarningIcon(props: IconComponentProps) { - return useSystemIcon('warning', props); + return ; } diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx index 3f0c01b56b..05960cd213 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx @@ -97,7 +97,7 @@ describe('ProxiedSignInPage', () => { render(Subject); await expect( - screen.findByText('Request failed with 401 Error'), + screen.findByText('Request failed with 401 Unauthorized'), ).resolves.toBeInTheDocument(); }); }); diff --git a/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts b/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts index db4731704c..6abb412090 100644 --- a/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts @@ -20,6 +20,9 @@ import { BackstageUserIdentity, } from '@backstage/core-plugin-api'; +/** + * @deprecated Use `@backstage/plugin-auth-backend-module-guest-provider` instead. + */ export class GuestUserIdentity implements IdentityApi { getUserId(): string { return 'guest'; diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index 5393c1ea89..563d1cc124 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -20,39 +20,108 @@ import Button from '@material-ui/core/Button'; import { InfoCard } from '../InfoCard/InfoCard'; import { GridItem } from './styles'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +import { ProxiedSignInIdentity } from '../ProxiedSignInPage/ProxiedSignInIdentity'; +import { discoveryApiRef, useApi } from '@backstage/core-plugin-api'; import { GuestUserIdentity } from './GuestUserIdentity'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { ResponseError } from '@backstage/errors'; -const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => ( - - { - onSignInStarted(); - onSignInSuccess(new GuestUserIdentity()); - }} - > - Enter - +const getIdentity = async (identity: ProxiedSignInIdentity) => { + try { + const identityResponse = await identity.getBackstageIdentity(); + return identityResponse; + } catch (error) { + if ( + error.name === 'ResponseError' && + (error as ResponseError).cause.name === 'NotFoundError' + ) { + return undefined; + } + throw error; + } +}; + +const Component: ProviderComponent = ({ + onSignInStarted, + onSignInSuccess, + onSignInFailure, +}) => { + const discoveryApi = useApi(discoveryApiRef); + const [_, setUseLegacyGuestToken] = useLocalStorage('enableLegacyGuestToken'); + + const handle = async () => { + onSignInStarted(); + + const identity = new ProxiedSignInIdentity({ + provider: 'guest', + discoveryApi, + }); + + const identityResponse = await getIdentity(identity); + + if (!identityResponse) { + // eslint-disable-next-line no-alert + const useLegacyGuestTokenResponse = confirm( + 'Failed to sign in as a guest using the auth backend. Do you want to fallback to the legacy guest token?', + ); + if (useLegacyGuestTokenResponse) { + setUseLegacyGuestToken(true); + onSignInSuccess(new GuestUserIdentity()); + return; } - > - - Enter as a Guest User. -
- You will not have a verified identity, -
- meaning some features might be unavailable. -
-
-
-); + onSignInFailure(); + throw new Error( + `You cannot sign in as a guest, you must either enable the legacy guest token or configure the auth backend to support guest sign in.`, + ); + } -const loader: ProviderLoader = async () => { - return new GuestUserIdentity(); + onSignInSuccess(identity); + }; + + return ( + + + Enter + + } + > + Sign in as a Guest. + + + ); +}; + +const loader: ProviderLoader = async apis => { + const useLegacyGuestToken = + localStorage.getItem('enableLegacyGuestToken') === 'true'; + + const identity = new ProxiedSignInIdentity({ + provider: 'guest', + discoveryApi: apis.get(discoveryApiRef)!, + }); + const identityResponse = await getIdentity(identity); + + if (!identityResponse && !useLegacyGuestToken) { + return undefined; + } else if (identityResponse && useLegacyGuestToken) { + // eslint-disable-next-line no-alert + const switchToNewGuestToken = confirm( + 'You are currently using the legacy guest token, but you have the new guest backend module installed. Do you want to use the new module?', + ); + if (switchToNewGuestToken) { + localStorage.removeItem('enableLegacyGuestToken'); + } else { + return new GuestUserIdentity(); + } + } else if (useLegacyGuestToken) { + return new GuestUserIdentity(); + } + + return identity; }; export const guestProvider: SignInProvider = { Component, loader }; diff --git a/packages/errors/api-report.md b/packages/errors/api-report.md index 3ece63035b..894aac8413 100644 --- a/packages/errors/api-report.md +++ b/packages/errors/api-report.md @@ -128,6 +128,10 @@ export class ResponseError extends Error { }, ): Promise; readonly response: ConsumedResponse; + // (undocumented) + readonly statusCode: number; + // (undocumented) + readonly statusText: string; } // @public diff --git a/packages/errors/src/errors/ResponseError.test.ts b/packages/errors/src/errors/ResponseError.test.ts index 94f2e850d9..38db084b03 100644 --- a/packages/errors/src/errors/ResponseError.test.ts +++ b/packages/errors/src/errors/ResponseError.test.ts @@ -35,6 +35,8 @@ describe('ResponseError', () => { const e = await ResponseError.fromResponse(response as Response); expect(e.name).toEqual('ResponseError'); expect(e.message).toEqual('Request failed with 444 Fours'); + expect(e.statusCode).toEqual(444); + expect(e.statusText).toEqual('Fours'); expect(e.cause.name).toEqual('Fours'); expect(e.cause.message).toEqual('Expected fives'); expect(e.cause.stack).toEqual('lines'); diff --git a/packages/errors/src/errors/ResponseError.ts b/packages/errors/src/errors/ResponseError.ts index f8ba0a1ba1..b5ca1e4ab8 100644 --- a/packages/errors/src/errors/ResponseError.ts +++ b/packages/errors/src/errors/ResponseError.ts @@ -53,6 +53,9 @@ export class ResponseError extends Error { */ readonly cause: Error; + readonly statusCode: number; + + readonly statusText: string; /** * Constructs a ResponseError based on a failed response. * @@ -65,9 +68,9 @@ export class ResponseError extends Error { ): Promise { const data = await parseErrorResponseBody(response); - const status = data.response.statusCode || response.status; - const statusText = data.error.name || response.statusText; - const message = `Request failed with ${status} ${statusText}`; + const statusCode = data.response.statusCode || response.status; + const statusText = response.statusText; + const message = `Request failed with ${statusCode} ${statusText}`; const cause = deserializeError(data.error); return new ResponseError({ @@ -75,19 +78,26 @@ export class ResponseError extends Error { response, data, cause, + statusCode, + statusText, }); } - private constructor(props: { + private constructor(opts: { message: string; response: ConsumedResponse; data: ErrorResponseBody; cause: Error; + statusCode: number; + statusText: string; }) { - super(props.message); + super(opts.message); + this.name = 'ResponseError'; - this.response = props.response; - this.body = props.data; - this.cause = props.cause; + this.response = opts.response; + this.body = opts.data; + this.cause = opts.cause; + this.statusCode = opts.statusCode; + this.statusText = opts.statusText; } } diff --git a/packages/integration-react/src/api/ScmAuth.test.ts b/packages/integration-react/src/api/ScmAuth.test.ts index 7e174c4cbf..d252b95b85 100644 --- a/packages/integration-react/src/api/ScmAuth.test.ts +++ b/packages/integration-react/src/api/ScmAuth.test.ts @@ -112,7 +112,8 @@ describe('ScmAuth', () => { await expect( azureAuth.getCredentials({ url: 'http://example.com' }), ).resolves.toMatchObject({ - token: 'vso.build vso.code vso.graph vso.project vso.profile', + token: + '499b84ac-1321-427f-aa17-267ca6975798/vso.build 499b84ac-1321-427f-aa17-267ca6975798/vso.code 499b84ac-1321-427f-aa17-267ca6975798/vso.graph 499b84ac-1321-427f-aa17-267ca6975798/vso.project 499b84ac-1321-427f-aa17-267ca6975798/vso.profile', }); await expect( azureAuth.getCredentials({ @@ -121,7 +122,7 @@ describe('ScmAuth', () => { }), ).resolves.toMatchObject({ token: - 'vso.build vso.code vso.graph vso.project vso.profile vso.code_manage', + '499b84ac-1321-427f-aa17-267ca6975798/vso.build 499b84ac-1321-427f-aa17-267ca6975798/vso.code 499b84ac-1321-427f-aa17-267ca6975798/vso.graph 499b84ac-1321-427f-aa17-267ca6975798/vso.project 499b84ac-1321-427f-aa17-267ca6975798/vso.profile 499b84ac-1321-427f-aa17-267ca6975798/vso.code_manage', }); const bitbucketAuth = ScmAuth.forBitbucket(mockAuthApi); @@ -174,10 +175,15 @@ describe('ScmAuth', () => { await expect( azureAuth.getCredentials({ url: 'http://example.com', - additionalScope: { customScopes: { azure: ['vso.org'] } }, + additionalScope: { + customScopes: { + azure: ['499b84ac-1321-427f-aa17-267ca6975798/vso.org'], + }, + }, }), ).resolves.toMatchObject({ - token: 'vso.build vso.code vso.graph vso.project vso.profile vso.org', + token: + '499b84ac-1321-427f-aa17-267ca6975798/vso.build 499b84ac-1321-427f-aa17-267ca6975798/vso.code 499b84ac-1321-427f-aa17-267ca6975798/vso.graph 499b84ac-1321-427f-aa17-267ca6975798/vso.project 499b84ac-1321-427f-aa17-267ca6975798/vso.profile 499b84ac-1321-427f-aa17-267ca6975798/vso.org', }); const bitbucketAuth = ScmAuth.forBitbucket(mockAuthApi); diff --git a/packages/integration-react/src/api/ScmAuth.ts b/packages/integration-react/src/api/ScmAuth.ts index b3dab4029e..a22b15e400 100644 --- a/packages/integration-react/src/api/ScmAuth.ts +++ b/packages/integration-react/src/api/ScmAuth.ts @@ -199,13 +199,13 @@ export class ScmAuth implements ScmAuthApi { const host = options?.host ?? 'dev.azure.com'; return new ScmAuth('azure', microsoftAuthApi, host, { default: [ - 'vso.build', - 'vso.code', - 'vso.graph', - 'vso.project', - 'vso.profile', + '499b84ac-1321-427f-aa17-267ca6975798/vso.build', + '499b84ac-1321-427f-aa17-267ca6975798/vso.code', + '499b84ac-1321-427f-aa17-267ca6975798/vso.graph', + '499b84ac-1321-427f-aa17-267ca6975798/vso.project', + '499b84ac-1321-427f-aa17-267ca6975798/vso.profile', ], - repoWrite: ['vso.code_manage'], + repoWrite: ['499b84ac-1321-427f-aa17-267ca6975798/vso.code_manage'], }); } diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md index 21d6be6a78..988569ece7 100644 --- a/packages/theme/api-report.md +++ b/packages/theme/api-report.md @@ -205,6 +205,9 @@ export const darkTheme: Theme_3; // @public export const defaultComponentThemes: ThemeOptions['components']; +// @public +export const defaultTypography: BackstageTypography; + // @public export function genPageTheme(props: { colors: string[]; diff --git a/packages/theme/src/base/createBaseThemeOptions.ts b/packages/theme/src/base/createBaseThemeOptions.ts index f0c8ab5fe9..ab02af61c8 100644 --- a/packages/theme/src/base/createBaseThemeOptions.ts +++ b/packages/theme/src/base/createBaseThemeOptions.ts @@ -22,6 +22,46 @@ const DEFAULT_FONT_FAMILY = '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif'; const DEFAULT_PAGE_THEME = 'home'; +/** + * Default Typography settings. + * + * @public + */ +export const defaultTypography: BackstageTypography = { + htmlFontSize: DEFAULT_HTML_FONT_SIZE, + fontFamily: DEFAULT_FONT_FAMILY, + h1: { + fontSize: 54, + fontWeight: 700, + marginBottom: 10, + }, + h2: { + fontSize: 40, + fontWeight: 700, + marginBottom: 8, + }, + h3: { + fontSize: 32, + fontWeight: 700, + marginBottom: 6, + }, + h4: { + fontWeight: 700, + fontSize: 28, + marginBottom: 6, + }, + h5: { + fontWeight: 700, + fontSize: 24, + marginBottom: 4, + }, + h6: { + fontWeight: 700, + fontSize: 20, + marginBottom: 2, + }, +}; + /** * Options for {@link createBaseThemeOptions}. * @@ -57,40 +97,8 @@ export function createBaseThemeOptions( throw new Error(`${defaultPageTheme} is not defined in pageTheme.`); } - const defaultTypography: BackstageTypography = { - htmlFontSize, - fontFamily, - h1: { - fontSize: 54, - fontWeight: 700, - marginBottom: 10, - }, - h2: { - fontSize: 40, - fontWeight: 700, - marginBottom: 8, - }, - h3: { - fontSize: 32, - fontWeight: 700, - marginBottom: 6, - }, - h4: { - fontWeight: 700, - fontSize: 28, - marginBottom: 6, - }, - h5: { - fontWeight: 700, - fontSize: 24, - marginBottom: 4, - }, - h6: { - fontWeight: 700, - fontSize: 20, - marginBottom: 2, - }, - }; + defaultTypography.htmlFontSize = htmlFontSize; + defaultTypography.fontFamily = fontFamily; return { palette, diff --git a/packages/theme/src/base/index.ts b/packages/theme/src/base/index.ts index 2da9fb0fac..f7a6b2fd07 100644 --- a/packages/theme/src/base/index.ts +++ b/packages/theme/src/base/index.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -export { createBaseThemeOptions } from './createBaseThemeOptions'; +export { + createBaseThemeOptions, + defaultTypography, +} from './createBaseThemeOptions'; export type { BaseThemeOptionsInput } from './createBaseThemeOptions'; export { colorVariants, genPageTheme, pageTheme, shapes } from './pageTheme'; export { palettes } from './palettes'; diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index 73def32c34..0216a8d4a1 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -41,14 +41,13 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "luxon": "^3.0.0", - "marked": "^4.0.14", + "marked": "^12.0.0", "node-fetch": "^2.6.5", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", - "@types/marked": "^5.0.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/adr-backend/src/search/madrParser.ts b/plugins/adr-backend/src/search/madrParser.ts index 0dd1437538..bb466d13c3 100644 --- a/plugins/adr-backend/src/search/madrParser.ts +++ b/plugins/adr-backend/src/search/madrParser.ts @@ -15,29 +15,27 @@ */ import { DateTime } from 'luxon'; -import { marked } from 'marked'; +import { marked, Tokens, TokensList } from 'marked'; import { MADR_DATE_FORMAT, parseMadrWithFrontmatter, } from '@backstage/plugin-adr-common'; -const getTitle = (tokens: marked.TokensList): string | undefined => { +const getTitle = (tokens: TokensList): string | undefined => { return ( - tokens.find( - t => t.type === 'heading' && t.depth === 1, - ) as marked.Tokens.Heading + tokens.find(t => t.type === 'heading' && t.depth === 1) as Tokens.Heading )?.text; }; -const getStatusForV2Format = (tokens: marked.TokensList): string | undefined => - (tokens.find(t => t.type === 'list') as marked.Tokens.List)?.items +const getStatusForV2Format = (tokens: TokensList): string | undefined => + (tokens.find(t => t.type === 'list') as Tokens.List)?.items ?.find(t => /^status:/i.test(t.text)) ?.text.replace(/^status:/i, '') .trim() .toLocaleLowerCase('en-US'); -const getDateForV2Format = (tokens: marked.TokensList): string | undefined => { - const listTokens = (tokens.find(t => t.type === 'list') as marked.Tokens.List) +const getDateForV2Format = (tokens: TokensList): string | undefined => { + const listTokens = (tokens.find(t => t.type === 'list') as Tokens.List) ?.items; const adrDateTime = listTokens ?.find(t => /^date:/i.test(t.text)) @@ -47,13 +45,13 @@ const getDateForV2Format = (tokens: marked.TokensList): string | undefined => { }; const getStatus = ( - tokens: marked.TokensList, + tokens: TokensList, frontMatterStatus?: string, ): string | undefined => { return frontMatterStatus ?? getStatusForV2Format(tokens); }; const getDate = ( - tokens: marked.TokensList, + tokens: TokensList, dateFormat: string, frontMatterDate?: string, ): string | undefined => { diff --git a/plugins/adr-common/api-report.md b/plugins/adr-common/api-report.md index a36854f0e7..f12a086359 100644 --- a/plugins/adr-common/api-report.md +++ b/plugins/adr-common/api-report.md @@ -25,6 +25,7 @@ export const ANNOTATION_ADR_LOCATION = 'backstage.io/adr-location'; export const getAdrLocationUrl: ( entity: Entity, scmIntegration: ScmIntegrationRegistry, + adrFilePath?: String, ) => string; // @public diff --git a/plugins/adr-common/src/index.ts b/plugins/adr-common/src/index.ts index 97c82b2047..85d61cd270 100644 --- a/plugins/adr-common/src/index.ts +++ b/plugins/adr-common/src/index.ts @@ -57,13 +57,20 @@ export const isAdrAvailable = (entity: Entity) => export const getAdrLocationUrl = ( entity: Entity, scmIntegration: ScmIntegrationRegistry, + adrFilePath?: String, ) => { if (!isAdrAvailable(entity)) { throw new Error(`Missing ADR annotation: ${ANNOTATION_ADR_LOCATION}`); } + let url = getAdrLocationDir(entity)!.replace(/\/$/, ''); + + if (adrFilePath) { + url = `${url}/${adrFilePath}`; + } + return scmIntegration.resolveUrl({ - url: getAdrLocationDir(entity)!, + url, base: getEntitySourceLocation(entity).target, }); }; diff --git a/plugins/adr/src/components/AdrReader/AdrReader.tsx b/plugins/adr/src/components/AdrReader/AdrReader.tsx index 6b07eee4cd..d0dd8b7444 100644 --- a/plugins/adr/src/components/AdrReader/AdrReader.tsx +++ b/plugins/adr/src/components/AdrReader/AdrReader.tsx @@ -45,11 +45,11 @@ export const AdrReader = (props: { const scmIntegrations = useApi(scmIntegrationsApiRef); const adrApi = useApi(adrApiRef); const adrLocationUrl = getAdrLocationUrl(entity, scmIntegrations); + const adrFileLocationUrl = getAdrLocationUrl(entity, scmIntegrations, adr); - const url = `${adrLocationUrl.replace(/\/$/, '')}/${adr}`; const { value, loading, error } = useAsync( - async () => adrApi.readAdr(url), - [url], + async () => adrApi.readAdr(adrFileLocationUrl), + [adrFileLocationUrl], ); const adrContent = useMemo(() => { diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index 38b8db89c6..8cb43e9108 100644 --- a/plugins/api-docs/src/alpha.tsx +++ b/plugins/api-docs/src/alpha.tsx @@ -30,7 +30,6 @@ import { compatWrapper, convertLegacyRouteRef, } from '@backstage/core-compat-api'; -import { useApp } from '@backstage/core-plugin-api'; import { createEntityCardExtension, @@ -45,17 +44,12 @@ import { import { defaultDefinitionWidgets } from './components/ApiDefinitionCard'; import { rootRoute, registerComponentRouteRef } from './routes'; import { apiDocsConfigRef } from './config'; - -function ApiIcon() { - const app = useApp(); - const KindApiSystemIcon = app.getSystemIcon('kind:api')!; - return ; -} +import { AppIcon } from '@backstage/core-components'; const apiDocsNavItem = createNavItemExtension({ title: 'APIs', routeRef: convertLegacyRouteRef(rootRoute), - icon: () => compatWrapper(), + icon: () => compatWrapper(), }); const apiDocsConfigApi = createApiExtension({ diff --git a/plugins/auth-backend-module-guest-provider/.eslintrc.js b/plugins/auth-backend-module-guest-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-guest-provider/README.md b/plugins/auth-backend-module-guest-provider/README.md new file mode 100644 index 0000000000..79591c987b --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/README.md @@ -0,0 +1,8 @@ +# Auth Module: Guest Provider + +This module provides a guest auth provider implementation for `@backstage/plugin-auth-backend`. This is meant to supersede the existing `'guest'` option for authentication that does not emit tokens and is completely stored as frontend state. + +## Links + +- [Backstage](https://backstage.io) +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-guest-provider) diff --git a/plugins/auth-backend-module-guest-provider/api-report.md b/plugins/auth-backend-module-guest-provider/api-report.md new file mode 100644 index 0000000000..773b80b9ba --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/api-report.md @@ -0,0 +1,11 @@ +## API Report File for "@backstage/plugin-auth-backend-module-guest-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @public (undocumented) +const authModuleGuestProvider: () => BackendFeature; +export default authModuleGuestProvider; +``` diff --git a/plugins/auth-backend-module-guest-provider/catalog-info.yaml b/plugins/auth-backend-module-guest-provider/catalog-info.yaml new file mode 100644 index 0000000000..5d3513296b --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-guest-provider + title: '@backstage/plugin-auth-backend-module-guest-provider' + description: The guest-provider backend module for the auth plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/auth-backend-module-guest-provider/config.d.ts b/plugins/auth-backend-module-guest-provider/config.d.ts new file mode 100644 index 0000000000..eb60492393 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/config.d.ts @@ -0,0 +1,42 @@ +/* + * 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 interface Config { + /** Configuration options for the auth plugin */ + auth?: { + providers: { + guest?: { + /** + * The entity reference to use for the guest user. + * @default user:development/guest + */ + userEntityRef?: string; + + /** + * A list of entity references to user for ownership of the guest user if the user + * is not found in the catalog. + * @default [userEntityRef] + */ + ownershipEntityRefs?: string[]; + + /** + * Allow users to sign in with the guest provider outside of their development environments. + */ + dangerouslyAllowOutsideDevelopment?: boolean; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json new file mode 100644 index 0000000000..bab85c2bd0 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/plugin-auth-backend-module-guest-provider", + "description": "The guest-provider backend module for the auth plugin.", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-guest-provider" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "passport-oauth2": "^1.7.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", + "express": "^4.18.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/auth-backend-module-guest-provider/src/authenticator.ts b/plugins/auth-backend-module-guest-provider/src/authenticator.ts new file mode 100644 index 0000000000..436b72617d --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/authenticator.ts @@ -0,0 +1,27 @@ +/* + * 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 { createProxyAuthenticator } from '@backstage/plugin-auth-node'; + +export const guestAuthenticator = createProxyAuthenticator({ + defaultProfileTransform: async () => { + return { profile: {} }; + }, + initialize() {}, + async authenticate() { + return { result: {} }; + }, +}); diff --git a/plugins/auth-backend-module-guest-provider/src/index.ts b/plugins/auth-backend-module-guest-provider/src/index.ts new file mode 100644 index 0000000000..c6ada31c3a --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * The guest-provider backend module for the auth plugin. + * + * @packageDocumentation + */ + +export { authModuleGuestProvider as default } from './module'; diff --git a/plugins/auth-backend-module-guest-provider/src/module.ts b/plugins/auth-backend-module-guest-provider/src/module.ts new file mode 100644 index 0000000000..7eac99b0a3 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/module.ts @@ -0,0 +1,56 @@ +/* + * 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 { + authProvidersExtensionPoint, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { guestAuthenticator } from './authenticator'; +import { signInAsGuestUser } from './resolvers'; + +/** @public */ +export const authModuleGuestProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'guest-provider', + register(reg) { + reg.registerInit({ + deps: { + logger: coreServices.logger, + providers: authProvidersExtensionPoint, + config: coreServices.rootConfig, + }, + async init({ providers, logger, config }) { + if (process.env.NODE_ENV !== 'development') { + logger.warn( + 'You should NOT be using the guest provider outside of a development environment.', + ); + } + providers.registerProvider({ + providerId: 'guest', + factory: createProxyAuthProviderFactory({ + authenticator: guestAuthenticator, + signInResolver: signInAsGuestUser( + config.getConfig('auth.providers.guest'), + ), + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-guest-provider/src/resolvers.ts b/plugins/auth-backend-module-guest-provider/src/resolvers.ts new file mode 100644 index 0000000000..35f724f746 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/resolvers.ts @@ -0,0 +1,59 @@ +/* + * 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 { stringifyEntityRef } from '@backstage/catalog-model'; +import type { Config } from '@backstage/config'; +import { SignInResolver } from '@backstage/plugin-auth-node'; +import { NotImplementedError } from '@backstage/errors'; + +/** + * Provide a default implementation of the user to resolve to. By default, this + * is `user:development/guest`. We will attempt to get that user if they're in the + * catalog. If that user doesn't exist in the catalog, we will still create a + * token for them so they can keep viewing. + */ +export const signInAsGuestUser: (config: Config) => SignInResolver<{}> = + (config: Config) => async (_, ctx) => { + if ( + process.env.NODE_ENV !== 'development' && + config.getOptionalBoolean('dangerouslyAllowOutsideDevelopment') !== true + ) { + throw new NotImplementedError( + 'The guest provider is NOT recommended for use outside of a development environment. If you want to enable this, set `auth.providers.guest.dangerouslyAllowOutsideDevelopment: true` in your app config.', + ); + } + const userRef = + config.getOptionalString('userEntityRef') ?? + stringifyEntityRef({ + kind: 'user', + namespace: 'development', + name: 'guest', + }); + const ownershipRefs = config.getOptionalStringArray( + 'ownershipEntityRefs', + ) ?? [userRef]; + try { + return ctx.signInWithCatalogUser({ entityRef: userRef }); + } catch (err) { + // We can't guarantee that a guest user exists in the catalog, so we issue a token directly, + return ctx.issueToken({ + claims: { + sub: userRef, + ent: ownershipRefs, + }, + }); + } + }; diff --git a/plugins/auth-backend-module-guest-provider/src/types.ts b/plugins/auth-backend-module-guest-provider/src/types.ts new file mode 100644 index 0000000000..c831014cb5 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/types.ts @@ -0,0 +1,21 @@ +/* + * 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 { ProfileTransform } from '@backstage/plugin-auth-node'; + +export interface GuestAuthenticator { + defaultProfileTransform: ProfileTransform<{}>; +} diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 4c8430b33f..f5f4dd29fd 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -185,6 +185,11 @@ export interface Config { /** @visibility frontend */ cfaccess?: { teamName: string; + /** @deepVisibility secret */ + serviceTokens?: Array<{ + token: string; + subject: string; + }>; }; /** * The backstage token expiration. diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 967233e586..456cc18af2 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -87,7 +87,7 @@ "passport-microsoft": "^1.0.0", "passport-oauth2": "^1.6.1", "passport-onelogin-oauth": "^0.0.1", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts index 95e4b0901b..f5f1233ecc 100644 --- a/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts +++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts @@ -34,6 +34,24 @@ const mockClaims = { exp: 1632833763, iss: 'ISSUER_URL', }; +const mockServiceTokenJwt = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWV9JRCIsImlzcyI6IklTU1VFUl9VUkwifQ.eyJzdWIiOiIiLCJuYW1lIjoiQm90IiwiY29tbW9uX25hbWUiOiJ0ZXN0X3Rva2VuX2lkLmFjY2VzcyIsImlhdCI6MTUxNjIzOTAyMn0.KEe-qBHuN8HKh1LobtDQnCJ3rxZOhW-lMSDad8uV_l0'; +const mockServiceTokenClaims = { + sub: '', + common_name: 'test_token_id.access', + iat: 1632833760, + exp: 1632833763, + iss: 'ISSUER_URL', +}; +const mockServiceTokenDisallowedJwt = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWV9JRCIsImlzcyI6IklTU1VFUl9VUkwifQ.eyJzdWIiOiIiLCJuYW1lIjoiQm90IiwiY29tbW9uX25hbWUiOiJzb21lX290aGVyX3Rva2VuX2lkLmFjY2VzcyIsImlhdCI6MTUxNjIzOTAyMn0.qQeeQW_urYrrTq-tuKZWURwTUrjzgyFyZA9ViQtD-FM'; +const mockServiceTokenDisallowedClaims = { + sub: '', + common_name: 'some_other_token_id.access', + iat: 1632833760, + exp: 1632833763, + iss: 'ISSUER_URL', +}; const mockCfIdentity = { name: 'foo', id: '123', @@ -78,6 +96,32 @@ const identityOkResponse = { }, }; +const identityOkServiceTokenResponse = { + backstageIdentity: { + expiresInSeconds: undefined, + identity: { + ownershipEntityRefs: ['user:default/jimmymarkum'], + type: 'user', + userEntityRef: 'user:default/jimmymarkum', + }, + token: + 'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob', + }, + profile: { + email: undefined, + }, + providerInfo: { + cfAccessIdentityProfile: { + email: 'test_token_id.access@foobar.com', + groups: [], + id: 'test_token_id.access', + name: 'Bot', + }, + claims: mockServiceTokenClaims, + expiresInSeconds: 3, + }, +}; + const mockAuthenticatedUserEmail = 'user.name@email.test'; const mockCacheClient = { get: jest.fn(), @@ -121,6 +165,18 @@ describe('CloudflareAccessAuthProvider', () => { }, } as unknown as express.Request; + const mockRequestWithSericeTokenJwtHeader = { + header: jest.fn(() => { + return mockServiceTokenJwt; + }), + } as unknown as express.Request; + + const mockRequestWithSericeTokenDisallowedJwtHeader = { + header: jest.fn(() => { + return mockServiceTokenDisallowedJwt; + }), + } as unknown as express.Request; + const mockRequestWithoutJwt = { header: jest.fn(_ => { return undefined; @@ -138,6 +194,7 @@ describe('CloudflareAccessAuthProvider', () => { const provider = new CloudflareAccessAuthProvider({ teamName: 'foobar', + serviceTokens: [], resolverContext: {} as AuthResolverContext, authHandler: async result => { expect(result).toEqual( @@ -169,7 +226,81 @@ describe('CloudflareAccessAuthProvider', () => { cache: mockCacheClient, }); + const providerServiceToken = new CloudflareAccessAuthProvider({ + teamName: 'foobar', + serviceTokens: [ + { + token: 'test_token_id.access', + subject: 'test_token_id.access@foobar.com', + }, + ], + resolverContext: {} as AuthResolverContext, + authHandler: async result => { + expect(result).toEqual( + expect.objectContaining({ + claims: mockServiceTokenClaims, + cfIdentity: { + email: 'test_token_id.access@foobar.com', + groups: [], + id: 'test_token_id.access', + name: 'Bot', + }, + token: mockServiceTokenJwt, + }), + ); + return { + profile: { + email: result.claims.email, + }, + }; + }, + signInResolver: async ({ result }) => { + expect(result).toEqual( + expect.objectContaining({ + claims: mockServiceTokenClaims, + cfIdentity: { + email: 'test_token_id.access@foobar.com', + groups: [], + id: 'test_token_id.access', + name: 'Bot', + }, + token: mockServiceTokenJwt, + }), + ); + return { + token: + 'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob', + }; + }, + cache: mockCacheClient, + }); + describe('when JWT is valid', () => { + it('validates a service token JWT without calling get-identity', async () => { + jwtMock.mockReturnValue( + Promise.resolve({ payload: mockServiceTokenClaims }), + ); + await providerServiceToken.refresh( + mockRequestWithSericeTokenJwtHeader, + mockResponse, + ); + expect(mockResponse.json).toHaveBeenCalledWith( + identityOkServiceTokenResponse, + ); + }); + + it('rejects a disallowed service token JWT without calling get-identity', async () => { + jwtMock.mockReturnValue( + Promise.resolve({ payload: mockServiceTokenDisallowedClaims }), + ); + await expect( + providerServiceToken.refresh( + mockRequestWithSericeTokenDisallowedJwtHeader, + mockResponse, + ), + ).rejects.toThrow(); + }); + it('returns cfidentity also when get-identity succeeds', async () => { jwtMock.mockReturnValue(Promise.resolve({ payload: mockClaims })); mockFetch.mockReturnValueOnce( diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts index fe271f9058..e4d317f8f6 100644 --- a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts +++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts @@ -17,7 +17,6 @@ import { AuthHandler } from '../types'; import fetch, { Headers } from 'node-fetch'; import express from 'express'; -import * as _ from 'lodash'; import { jwtVerify, createRemoteJWKSet } from 'jose'; import { AuthenticationError, @@ -49,6 +48,11 @@ const CACHE_PREFIX = 'providers/cloudflare-access/profile-v1'; */ export const CF_DEFAULT_CACHE_TTL = 3600; +type ServiceToken = { + token: string; + subject: string; +}; + /** @public */ export type Options = { /** @@ -59,6 +63,15 @@ export type Options = { * https://.cloudflareaccess.com/cdn-cgi/access/certs */ teamName: string; + /** + * Allowed Cloudflare Service Tokens + * + * Cloudflare does not currently allow assigning any sort of identity to + * Service Tokens. Therefore, this allows you to build an allow list mapping + * the Client ID of any Service Tokens that should be allowed to pass the + * auth check to the identity (email) you would like to associate with it. + */ + serviceTokens: ServiceToken[]; authHandler: AuthHandler; signInResolver: SignInResolver; resolverContext: AuthResolverContext; @@ -179,6 +192,7 @@ export type CloudflareAccessResponse = export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers { private readonly teamName: string; + private readonly serviceTokens: ServiceToken[]; private readonly resolverContext: AuthResolverContext; private readonly authHandler: AuthHandler; private readonly signInResolver: SignInResolver; @@ -187,6 +201,7 @@ export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers { constructor(options: Options) { this.teamName = options.teamName; + this.serviceTokens = options.serviceTokens; this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; this.resolverContext = options.resolverContext; @@ -260,8 +275,27 @@ export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers { const verifyResult = await jwtVerify(jwt, this.jwtKeySet, { issuer: `https://${this.teamName}.cloudflareaccess.com`, }); - const sub = verifyResult.payload.sub; - const cfAccessResultStr = await this.cache?.get(`${CACHE_PREFIX}/${sub}`); + + const isServiceToken = !verifyResult.payload.sub; + + const subject = isServiceToken + ? (verifyResult.payload.common_name as string) + : verifyResult.payload.sub; + if (!subject) { + throw new AuthenticationError( + `Missing both sub and common_name from Cloudflare Access JWT`, + ); + } + + const serviceToken = this.serviceTokens.find(st => st.token === subject); + if (isServiceToken && !serviceToken) { + throw new AuthenticationError( + `${subject} is not a permitted Service Token.`, + ); + } + + const cacheKey = `${CACHE_PREFIX}/${subject}`; + const cfAccessResultStr = await this.cache?.get(cacheKey); if (typeof cfAccessResultStr === 'string') { const result = JSON.parse(cfAccessResultStr) as CloudflareAccessResult; return { @@ -270,12 +304,23 @@ export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers { }; } const claims = verifyResult.payload as CloudflareAccessClaims; + // Builds a passport profile from JWT claims first try { - // If we successfully fetch the get-identity endpoint, - // We supplement the passport profile with richer user identity - // information here. - const cfIdentity = await this.getIdentityProfile(jwt); + let cfIdentity: CloudflareAccessIdentityProfile; + if (serviceToken) { + cfIdentity = { + id: subject, + name: 'Bot', + email: serviceToken.subject, + groups: [], + }; + } else { + // If we successfully fetch the get-identity endpoint, + // We supplement the passport profile with richer user identity + // information here. + cfIdentity = await this.getIdentityProfile(jwt); + } // Stores a stringified JSON object in cfaccess provider cache only when // we complete all steps const cfAccessResult = { @@ -283,7 +328,7 @@ export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers { cfIdentity, expiresInSeconds: claims.exp - claims.iat, }; - this.cache?.set(`${CACHE_PREFIX}/${sub}`, JSON.stringify(cfAccessResult)); + this.cache?.set(cacheKey, JSON.stringify(cfAccessResult)); return { ...cfAccessResult, token: jwt, @@ -350,6 +395,15 @@ export const cfAccess = createAuthProviderIntegration({ }) { return ({ config, resolverContext }) => { const teamName = config.getString('teamName'); + const serviceTokensConfig = + config.getOptionalConfigArray('serviceTokens'); + const serviceTokens = + serviceTokensConfig?.map(cfg => { + return { + token: cfg.getString('token'), + subject: cfg.getString('subject'), + } as ServiceToken; + }) || []; if (!options.signIn.resolver) { throw new Error( @@ -371,6 +425,7 @@ export const cfAccess = createAuthProviderIntegration({ return new CloudflareAccessAuthProvider({ teamName, + serviceTokens, signInResolver: options?.signIn.resolver, authHandler, resolverContext, diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index add7087ace..7830126365 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -57,6 +57,6 @@ "lodash": "^4.17.21", "msw": "^1.0.0", "supertest": "^6.1.3", - "uuid": "^8.0.0" + "uuid": "^9.0.0" } } diff --git a/plugins/azure-sites-backend/README.md b/plugins/azure-sites-backend/README.md index 892cbe4024..18c51239c3 100644 --- a/plugins/azure-sites-backend/README.md +++ b/plugins/azure-sites-backend/README.md @@ -33,6 +33,8 @@ Configuration Details: Here's how to get the backend plugin up and running: +#### Legacy Backend System + 1. First we need to add the `@backstage/plugin-azure-sites-backend` package to your backend: ```sh @@ -49,14 +51,17 @@ Here's how to get the backend plugin up and running: } from '@backstage/plugin-azure-sites-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; + import { CatalogClient } from '@backstage/catalog-client'; export default async function createPlugin( env: PluginEnvironment, ): Promise { + const catalogApi = new CatalogClient({ discoveryApi: env.discovery }); return await createRouter({ logger: env.logger, azureSitesApi: AzureSitesApi.fromConfig(env.config), permissions: env.permissions, + catalogApi, }); } ``` @@ -105,6 +110,28 @@ Here's how to get the backend plugin up and running: } ``` -5. Now run `yarn start-backend` from the repo root. +#### New Backend System -6. Finally, open `http://localhost:7007/api/azure/health` in a browser, it should return `{"status":"ok"}`. +The Azure Sites backend plugin has support for the [new backend system](https://backstage.io/docs/backend-system/), here's how you can set that up: + +In your `packages/backend/src/index.ts` make the following changes: + +```diff + import { createBackend } from '@backstage/backend-defaults'; + + const backend = createBackend(); + + // ... other feature additions + ++ backend.add(import('@backstage/plugin-azure-sites-backend')); + + // ... + + backend.start(); +``` + +#### Start Backed & Test + +1. Now run `yarn start-backend` from the repo root. + +2. Finally, open `http://localhost:7007/api/azure/health` in a browser, it should return `{"status":"ok"}`. diff --git a/plugins/azure-sites-backend/api-report.md b/plugins/azure-sites-backend/api-report.md index 04cd22596a..611a106816 100644 --- a/plugins/azure-sites-backend/api-report.md +++ b/plugins/azure-sites-backend/api-report.md @@ -3,14 +3,18 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { AzureSiteListRequest } from '@backstage/plugin-azure-sites-common'; import { AzureSiteListResponse } from '@backstage/plugin-azure-sites-common'; import { AzureSiteStartStopRequest } from '@backstage/plugin-azure-sites-common'; +import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; -import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { PermissionsService } from '@backstage/backend-plugin-api'; // @public (undocumented) export class AzureSitesApi { @@ -50,19 +54,29 @@ export class AzureSitesConfig { readonly tenantId: string; } +// @public +const azureSitesPlugin: () => BackendFeature; +export default azureSitesPlugin; + // @public (undocumented) export function createRouter(options: RouterOptions): Promise; // @public (undocumented) export interface RouterOptions { + // (undocumented) + auth?: AuthService; // (undocumented) azureSitesApi: AzureSitesApi; // (undocumented) catalogApi: CatalogApi; // (undocumented) + discovery: DiscoveryService; + // (undocumented) + httpAuth?: HttpAuthService; + // (undocumented) logger: Logger; // (undocumented) - permissions: PermissionEvaluator; + permissions: PermissionsService; } // (No @packageDocumentation comment for this package) diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json index 2e9a45a33c..46cd4a5e79 100644 --- a/plugins/azure-sites-backend/package.json +++ b/plugins/azure-sites-backend/package.json @@ -36,12 +36,14 @@ "@azure/arm-resourcegraph": "^4.2.1", "@azure/identity": "^4.0.0", "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-azure-sites-common": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", "@types/express": "^4.17.6", diff --git a/plugins/azure-sites-backend/src/index.ts b/plugins/azure-sites-backend/src/index.ts index 97f94e4470..7f56b8e92a 100644 --- a/plugins/azure-sites-backend/src/index.ts +++ b/plugins/azure-sites-backend/src/index.ts @@ -17,3 +17,4 @@ export * from './service/router'; export * from './api'; export * from './config'; +export { azureSitesPlugin as default } from './plugin'; diff --git a/plugins/azure-sites-backend/src/plugin.ts b/plugins/azure-sites-backend/src/plugin.ts new file mode 100644 index 0000000000..a8afe4e430 --- /dev/null +++ b/plugins/azure-sites-backend/src/plugin.ts @@ -0,0 +1,69 @@ +/* + * 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 { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { AzureSitesApi } from './api'; + +/** + * Azure Sites Backend Plugin + * + * @public + */ +export const azureSitesPlugin = createBackendPlugin({ + pluginId: 'azure-sites', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + permissions: coreServices.permissions, + discovery: coreServices.discovery, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + catalogApi: catalogServiceRef, + }, + async init({ + config, + logger, + httpRouter, + permissions, + catalogApi, + discovery, + auth, + httpAuth, + }) { + const azureSitesApi = AzureSitesApi.fromConfig(config); + httpRouter.use( + await createRouter({ + logger: loggerToWinstonLogger(logger), + azureSitesApi, + permissions, + catalogApi, + discovery, + auth, + httpAuth, + }), + ); + }, + }); + }, +}); diff --git a/plugins/azure-sites-backend/src/service/router.ts b/plugins/azure-sites-backend/src/service/router.ts index bd5d20c05e..79e4f46c04 100644 --- a/plugins/azure-sites-backend/src/service/router.ts +++ b/plugins/azure-sites-backend/src/service/router.ts @@ -14,17 +14,16 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { + createLegacyAuthAdapters, + errorHandler, +} from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { InputError, NotAllowedError, NotFoundError } from '@backstage/errors'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; -import { - PermissionEvaluator, - AuthorizeResult, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { azureSitesActionPermission, azureSitesPermissions, @@ -34,13 +33,22 @@ import { createPermissionIntegrationRouter } from '@backstage/plugin-permission- import { CatalogApi } from '@backstage/catalog-client'; import { AzureSitesApi } from '../api'; +import { + DiscoveryService, + AuthService, + HttpAuthService, + PermissionsService, +} from '@backstage/backend-plugin-api'; /** @public */ export interface RouterOptions { logger: Logger; azureSitesApi: AzureSitesApi; catalogApi: CatalogApi; - permissions: PermissionEvaluator; + permissions: PermissionsService; + discovery: DiscoveryService; + auth?: AuthService; + httpAuth?: HttpAuthService; } /** @public */ @@ -48,6 +56,7 @@ export async function createRouter( options: RouterOptions, ): Promise { const { logger, azureSitesApi, permissions, catalogApi } = options; + const { auth, httpAuth } = createLegacyAuthAdapters(options); const permissionIntegrationRouter = createPermissionIntegrationRouter({ permissions: azureSitesPermissions, @@ -73,13 +82,15 @@ export async function createRouter( '/:subscription/:resourceGroup/:name/start', async (request, response) => { const { subscription, resourceGroup, name } = request.params; - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); + const credentials = await httpAuth.credentials(request); const entityRef = request.body.entityRef; if (typeof entityRef !== 'string') { throw new InputError('Invalid entityRef, not a string'); } + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'catalog', + }); const entity = await catalogApi.getEntityByRef(entityRef, { token }); if (entity) { @@ -101,9 +112,7 @@ export async function createRouter( resourceRef: entityRef, }, ], - { - token, - }, + { credentials }, ) )[0] : undefined; @@ -130,14 +139,16 @@ export async function createRouter( '/:subscription/:resourceGroup/:name/stop', async (request, response) => { const { subscription, resourceGroup, name } = request.params; - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); + const credentials = await httpAuth.credentials(request); const entityRef = request.body.entityRef; if (typeof entityRef !== 'string') { throw new InputError('Invalid entityRef, not a string'); } + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'catalog', + }); const entity = await catalogApi.getEntityByRef(entityRef, { token }); if (entity) { @@ -160,9 +171,7 @@ export async function createRouter( resourceRef: entityRef, }, ], - { - token, - }, + { credentials }, ) )[0] : undefined; diff --git a/plugins/azure-sites-backend/src/service/standaloneServer.ts b/plugins/azure-sites-backend/src/service/standaloneServer.ts index 202c1a128d..c0961f0359 100644 --- a/plugins/azure-sites-backend/src/service/standaloneServer.ts +++ b/plugins/azure-sites-backend/src/service/standaloneServer.ts @@ -53,6 +53,7 @@ export async function startStandaloneServer( permissions, azureSitesApi: AzureSitesApi.fromConfig(config), catalogApi, + discovery, }); let service = createServiceBuilder(module) diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 346daa2d09..19e4571d96 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -67,7 +67,7 @@ "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "p-limit": "^3.0.2", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 08b1dceeb5..2d92d7284f 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -57,7 +57,7 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md index 22eeddea3a..22845cf260 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md @@ -7,18 +7,15 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; -import { EventParams } from '@backstage/plugin-events-node'; import { Events } from '@backstage/plugin-bitbucket-cloud-common'; -import { EventSubscriber } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; +import { EventsService } from '@backstage/plugin-events-node'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; import { TokenManager } from '@backstage/backend-common'; // @public -export class BitbucketCloudEntityProvider - implements EntityProvider, EventSubscriber -{ +export class BitbucketCloudEntityProvider implements EntityProvider { // (undocumented) connect(connection: EntityProviderConnection): Promise; // (undocumented) @@ -26,7 +23,8 @@ export class BitbucketCloudEntityProvider config: Config, options: { catalogApi?: CatalogApi; - logger: Logger; + events?: EventsService; + logger: LoggerService; schedule?: TaskRunner; scheduler?: PluginTaskScheduler; tokenManager?: TokenManager; @@ -37,12 +35,8 @@ export class BitbucketCloudEntityProvider // (undocumented) getTaskId(): string; // (undocumented) - onEvent(params: EventParams): Promise; - // (undocumented) onRepoPush(event: Events.RepoPushEvent): Promise; // (undocumented) - refresh(logger: Logger): Promise; - // (undocumented) - supportsEventTopics(): string[]; + refresh(logger: LoggerService): Promise; } ``` diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index b5e75b517f..441781331e 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -56,13 +56,13 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-events-node": "workspace:^", - "uuid": "^8.0.0", - "winston": "^3.2.1" + "uuid": "^9.0.0" }, "devDependencies": { "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", "luxon": "^3.0.0", "msw": "^1.0.0" }, 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 c771a40acc..9fff322eab 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 @@ -14,18 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { startTestBackend, mockServices } from '@backstage/backend-test-utils'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { eventsExtensionPoint } from '@backstage/plugin-events-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'; describe('catalogModuleBitbucketCloudEntityProvider', () => { it('should register provider at the catalog extension point', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); let addedProviders: Array | undefined; - let addedSubscribers: Array | undefined; let usedSchedule: TaskScheduleDefinition | undefined; const catalogExtensionPointImpl = { @@ -33,11 +43,7 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { addedProviders = providers; }, }; - const eventsExtensionPointImpl = { - addSubscribers: (subscribers: any) => { - addedSubscribers = subscribers; - }, - }; + const connection = jest.fn() as unknown as EntityProviderConnection; const runner = jest.fn(); const scheduler = mockServices.scheduler.mock({ createScheduledTaskRunner(schedule) { @@ -49,9 +55,9 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { await startTestBackend({ extensionPoints: [ [catalogProcessingExtensionPoint, catalogExtensionPointImpl], - [eventsExtensionPoint, eventsExtensionPointImpl], ], features: [ + eventsServiceFactory(), catalogModuleBitbucketCloudEntityProvider(), mockServices.rootConfig.factory({ data: { @@ -75,10 +81,14 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); expect(addedProviders?.length).toEqual(1); - expect(addedProviders?.pop()?.getProviderName()).toEqual( + expect(runner).not.toHaveBeenCalled(); + const provider = addedProviders!.pop()!; + expect(provider.getProviderName()).toEqual( 'bitbucketCloud-provider:default', ); - expect(addedSubscribers).toEqual(addedProviders); - expect(runner).not.toHaveBeenCalled(); + await provider.connect(connection); + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('bitbucketCloud-provider:default'); + expect(runner).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts index 39265dc1a5..21d86ce8e6 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { loggerToWinstonLogger } from '@backstage/backend-common'; import { coreServices, createBackendModule, @@ -23,7 +22,7 @@ import { catalogProcessingExtensionPoint, catalogServiceRef, } from '@backstage/plugin-catalog-node/alpha'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { BitbucketCloudEntityProvider } from '../providers/BitbucketCloudEntityProvider'; /** @@ -38,9 +37,7 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({ catalog: catalogProcessingExtensionPoint, catalogApi: catalogServiceRef, config: coreServices.rootConfig, - // TODO(pjungermann): How to make this optional for those which only want the provider without event support? - // Do we even want to support this? - events: eventsExtensionPoint, + events: eventsServiceRef, logger: coreServices.logger, scheduler: coreServices.scheduler, tokenManager: coreServices.tokenManager, @@ -54,16 +51,15 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({ scheduler, tokenManager, }) { - const winstonLogger = loggerToWinstonLogger(logger); const providers = BitbucketCloudEntityProvider.fromConfig(config, { catalogApi, - logger: winstonLogger, + events, + logger, scheduler, tokenManager, }); catalog.addEntityProvider(providers); - events.addSubscribers(providers); }, }); }, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index 573f21ce60..6a21d95afa 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -29,6 +29,7 @@ import { locationSpecToLocationEntity, } from '@backstage/plugin-catalog-node'; import { Events } from '@backstage/plugin-bitbucket-cloud-common'; +import { DefaultEventsService } from '@backstage/plugin-events-node'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { @@ -436,6 +437,7 @@ describe('BitbucketCloudEntityProvider', () => { 'added-module/catalog-custom.yaml', ); + const events = DefaultEventsService.create({ logger }); const catalogApi = { getEntities: async ( request: { filter: Record }, @@ -457,6 +459,7 @@ describe('BitbucketCloudEntityProvider', () => { }; const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { catalogApi: catalogApi as any as CatalogApi, + events, logger, schedule, tokenManager, @@ -537,7 +540,7 @@ describe('BitbucketCloudEntityProvider', () => { ); await provider.connect(entityProviderConnection); - await provider.onEvent(repoPushEventParams); + await events.publish(repoPushEventParams); const addedEntities = [ { @@ -566,31 +569,22 @@ describe('BitbucketCloudEntityProvider', () => { }); }); - it('onRepoPush fail on incomplete setup', async () => { - const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { - logger, - schedule, - })[0]; - - await expect(provider.onEvent(repoPushEventParams)).rejects.toThrow( - 'bitbucketCloud-provider:myProvider not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.', - ); - }); - it('no onRepoPush update on non-matching workspace slug', async () => { const catalogApi = { getEntities: jest.fn(), refreshEntity: jest.fn(), }; + const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { catalogApi: catalogApi as any as CatalogApi, + events, logger, schedule, tokenManager, })[0]; await provider.connect(entityProviderConnection); - await provider.onEvent({ + await events.publish({ ...repoPushEventParams, eventPayload: { ...repoPushEventParams.eventPayload, @@ -613,15 +607,17 @@ describe('BitbucketCloudEntityProvider', () => { getEntities: jest.fn(), refreshEntity: jest.fn(), }; + const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { catalogApi: catalogApi as any as CatalogApi, + events, logger, schedule, tokenManager, })[0]; await provider.connect(entityProviderConnection); - await provider.onEvent({ + await events.publish({ ...repoPushEventParams, eventPayload: { ...repoPushEventParams.eventPayload, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts index 14f561473b..9135e0717f 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts @@ -15,6 +15,7 @@ */ import { TokenManager } from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; import { CatalogApi } from '@backstage/catalog-client'; import { LocationEntity } from '@backstage/catalog-model'; @@ -35,13 +36,12 @@ import { locationSpecToLocationEntity, } from '@backstage/plugin-catalog-node'; import { LocationSpec } from '@backstage/plugin-catalog-common'; -import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { BitbucketCloudEntityProviderConfig, readProviderConfigs, } from './BitbucketCloudEntityProviderConfig'; import * as uuid from 'uuid'; -import { Logger } from 'winston'; const DEFAULT_BRANCH = 'master'; const TOPIC_REPO_PUSH = 'bitbucketCloud.repo:push'; @@ -62,14 +62,13 @@ interface IngestionTarget { * * @public */ -export class BitbucketCloudEntityProvider - implements EntityProvider, EventSubscriber -{ +export class BitbucketCloudEntityProvider implements EntityProvider { private readonly client: BitbucketCloudClient; private readonly config: BitbucketCloudEntityProviderConfig; - private readonly logger: Logger; + private readonly logger: LoggerService; private readonly scheduleFn: () => Promise; private readonly catalogApi?: CatalogApi; + private readonly events?: EventsService; private readonly tokenManager?: TokenManager; private connection?: EntityProviderConnection; @@ -79,7 +78,8 @@ export class BitbucketCloudEntityProvider config: Config, options: { catalogApi?: CatalogApi; - logger: Logger; + events?: EventsService; + logger: LoggerService; schedule?: TaskRunner; scheduler?: PluginTaskScheduler; tokenManager?: TokenManager; @@ -114,6 +114,7 @@ export class BitbucketCloudEntityProvider options.logger, taskRunner, options.catalogApi, + options.events, options.tokenManager, ); }); @@ -122,9 +123,10 @@ export class BitbucketCloudEntityProvider private constructor( config: BitbucketCloudEntityProviderConfig, integration: BitbucketCloudIntegration, - logger: Logger, + logger: LoggerService, taskRunner: TaskRunner, catalogApi?: CatalogApi, + events?: EventsService, tokenManager?: TokenManager, ) { this.client = BitbucketCloudClient.fromConfig(integration.config); @@ -134,6 +136,7 @@ export class BitbucketCloudEntityProvider }); this.scheduleFn = this.createScheduleFn(taskRunner); this.catalogApi = catalogApi; + this.events = events; this.tokenManager = tokenManager; } @@ -176,9 +179,23 @@ export class BitbucketCloudEntityProvider async connect(connection: EntityProviderConnection): Promise { this.connection = connection; await this.scheduleFn(); + + if (this.events) { + await this.events.subscribe({ + id: this.getProviderName(), + topics: [TOPIC_REPO_PUSH], + onEvent: async params => { + if (params.topic !== TOPIC_REPO_PUSH) { + return; + } + + await this.onRepoPush(params.eventPayload as Events.RepoPushEvent); + }, + }); + } } - async refresh(logger: Logger) { + async refresh(logger: LoggerService) { if (!this.connection) { throw new Error('Not initialized'); } @@ -198,20 +215,6 @@ export class BitbucketCloudEntityProvider ); } - /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.supportsEventTopics} */ - supportsEventTopics(): string[] { - return [TOPIC_REPO_PUSH]; - } - - /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.onEvent} */ - async onEvent(params: EventParams): Promise { - if (params.topic !== TOPIC_REPO_PUSH) { - return; - } - - await this.onRepoPush(params.eventPayload as Events.RepoPushEvent); - } - private canHandleEvents(): boolean { if (this.catalogApi && this.tokenManager) { return true; diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 46548b3df5..795652603d 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -54,7 +54,7 @@ "@backstage/plugin-catalog-node": "workspace:^", "@types/node-fetch": "^2.5.12", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 3337176653..b0c0ff81ea 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -54,7 +54,7 @@ "@backstage/plugin-catalog-node": "workspace:^", "fs-extra": "^11.2.0", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index b6a8f3acfe..6f3b933aa8 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -66,7 +66,7 @@ "lodash": "^4.17.21", "minimatch": "^9.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 661ed43f0e..76a20bd97f 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -54,14 +54,14 @@ "@backstage/plugin-catalog-node": "workspace:^", "lodash": "^4.17.21", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", - "@types/uuid": "^8.0.0", + "@types/uuid": "^9.0.0", "luxon": "^3.0.0", "msw": "^1.0.0" }, diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index e4d7b7efcf..02963f6bc1 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -65,7 +65,7 @@ "express-promise-router": "^4.1.0", "knex": "^3.0.0", "luxon": "^3.0.0", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 9cae290571..7720e97aa6 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -42,7 +42,7 @@ "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", "lodash": "^4.17.21", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 95070e6d78..b2c09bf201 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -63,7 +63,7 @@ "node-fetch": "^2.6.7", "p-limit": "^3.0.2", "qs": "^6.9.4", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index 4e127953bc..c971e58299 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -62,7 +62,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 92a2fbb280..ef2379766d 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -11,6 +11,7 @@ import { AnalyzeLocationGenerateEntity as AnalyzeLocationGenerateEntity_2 } from import { AnalyzeLocationRequest as AnalyzeLocationRequest_2 } from '@backstage/plugin-catalog-common'; import { AnalyzeLocationResponse as AnalyzeLocationResponse_2 } from '@backstage/plugin-catalog-common'; import { AnalyzeOptions as AnalyzeOptions_2 } from '@backstage/plugin-catalog-node'; +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogCollatorEntityTransformer as CatalogCollatorEntityTransformer_2 } from '@backstage/plugin-search-backend-module-catalog'; import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; @@ -28,6 +29,7 @@ import { Config } from '@backstage/config'; import { DefaultCatalogCollatorFactory as DefaultCatalogCollatorFactory_2 } from '@backstage/plugin-search-backend-module-catalog'; import { DefaultCatalogCollatorFactoryOptions as DefaultCatalogCollatorFactoryOptions_2 } from '@backstage/plugin-search-backend-module-catalog'; import { DeferredEntity as DeferredEntity_2 } from '@backstage/plugin-catalog-node'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import { EntitiesSearchFilter as EntitiesSearchFilter_2 } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter as EntityFilter_2 } from '@backstage/plugin-catalog-node'; @@ -38,15 +40,16 @@ import { EntityProviderMutation as EntityProviderMutation_2 } from '@backstage/p import { EntityRelationSpec as EntityRelationSpec_2 } from '@backstage/plugin-catalog-node'; import { EventBroker } from '@backstage/plugin-events-node'; import { GetEntitiesRequest } from '@backstage/catalog-client'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common'; import { locationSpecToLocationEntity as locationSpecToLocationEntity_2 } from '@backstage/plugin-catalog-node'; import { locationSpecToMetadataName as locationSpecToMetadataName_2 } from '@backstage/plugin-catalog-node'; import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; -import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; +import { PermissionsService } from '@backstage/backend-plugin-api'; import { PlaceholderResolver as PlaceholderResolver_2 } from '@backstage/plugin-catalog-node'; import { PlaceholderResolverParams as PlaceholderResolverParams_2 } from '@backstage/plugin-catalog-node'; import { PlaceholderResolverRead as PlaceholderResolverRead_2 } from '@backstage/plugin-catalog-node'; @@ -189,8 +192,11 @@ export type CatalogEnvironment = { database: PluginDatabaseManager; config: Config; reader: UrlReader; - permissions: PermissionEvaluator | PermissionAuthorizer; + permissions: PermissionsService | PermissionAuthorizer; scheduler?: PluginTaskScheduler; + discovery?: DiscoveryService; + auth?: AuthService; + httpAuth?: HttpAuthService; }; // @public diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index fe72f0938c..7253f2db31 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -80,7 +80,7 @@ "node-fetch": "^2.6.7", "p-limit": "^3.0.2", "prom-client": "^15.0.0", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yaml": "^2.0.0", "yn": "^4.0.0", @@ -95,7 +95,7 @@ "@types/glob": "^8.0.0", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", - "@types/uuid": "^8.0.0", + "@types/uuid": "^9.0.0", "better-sqlite3": "^9.0.0", "luxon": "^3.0.0", "msw": "^1.0.0", diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index e9f5f154c2..9cc83098b3 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '@backstage/plugin-catalog-node'; @@ -48,7 +49,7 @@ export type EntitiesRequest = { fields?: (entity: Entity) => Entity; order?: EntityOrder[]; pagination?: EntityPagination; - authorizationToken?: string; + credentials: BackstageCredentials; }; export type EntitiesResponse = { @@ -75,9 +76,9 @@ export interface EntitiesBatchRequest { */ fields?: (entity: Entity) => Entity; /** - * The optional token that authorizes the action. + * The credentials that authorizes the action. */ - authorizationToken?: string; + credentials: BackstageCredentials; } export interface EntitiesBatchResponse { @@ -115,9 +116,9 @@ export interface EntityFacetsRequest { */ facets: string[]; /** - * The optional token that authorizes the action. + * The credentials that authorizes the action. */ - authorizationToken?: string; + credentials: BackstageCredentials; } /** @@ -157,7 +158,7 @@ export interface EntitiesCatalog { */ removeEntityByUid( uid: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; /** @@ -167,7 +168,7 @@ export interface EntitiesCatalog { */ entityAncestry( entityRef: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; /** @@ -192,7 +193,7 @@ export type QueryEntitiesRequest = * for the current and the next pagination requests. */ export interface QueryEntitiesInitialRequest { - authorizationToken?: string; + credentials: BackstageCredentials; fields?: (entity: Entity) => Entity; limit?: number; filter?: EntityFilter; @@ -208,7 +209,7 @@ export interface QueryEntitiesInitialRequest { * move forward or backward on the data. */ export interface QueryEntitiesCursorRequest { - authorizationToken?: string; + credentials: BackstageCredentials; fields?: (entity: Entity) => Entity; limit?: number; cursor: Cursor; diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts index 4fe0311fff..3e2dee7ae0 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts @@ -23,6 +23,7 @@ import { AuthorizedEntitiesCatalog } from './AuthorizedEntitiesCatalog'; import { Cursor, QueryEntitiesResponse } from '../catalog/types'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '@backstage/plugin-catalog-node'; +import { mockCredentials } from '@backstage/backend-test-utils'; describe('AuthorizedEntitiesCatalog', () => { const fakeCatalog = { @@ -60,7 +61,7 @@ describe('AuthorizedEntitiesCatalog', () => { expect( await catalog.entities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }), ).toEqual({ entities: [], @@ -77,10 +78,10 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(isEntityKind); - await catalog.entities({ authorizationToken: 'abcd' }); + await catalog.entities({ credentials: mockCredentials.none() }); expect(fakeCatalog.entities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -91,10 +92,10 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(); - await catalog.entities({ authorizationToken: 'abcd' }); + await catalog.entities({ credentials: mockCredentials.none() }); expect(fakeCatalog.entities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); }); }); @@ -109,7 +110,7 @@ describe('AuthorizedEntitiesCatalog', () => { await expect( catalog.entitiesBatch({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }), ).resolves.toEqual({ items: [null], @@ -132,12 +133,12 @@ describe('AuthorizedEntitiesCatalog', () => { await catalog.entitiesBatch({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); expect(fakeCatalog.entitiesBatch).toHaveBeenCalledWith({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -150,12 +151,12 @@ describe('AuthorizedEntitiesCatalog', () => { await catalog.entitiesBatch({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); expect(fakeCatalog.entitiesBatch).toHaveBeenCalledWith({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); }); }); @@ -169,7 +170,7 @@ describe('AuthorizedEntitiesCatalog', () => { await expect( catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }), ).resolves.toEqual({ @@ -188,12 +189,12 @@ describe('AuthorizedEntitiesCatalog', () => { const catalog = createCatalog(); await catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -243,12 +244,12 @@ describe('AuthorizedEntitiesCatalog', () => { const catalog = createCatalog(isEntityKind); let response = await catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'name', values: ['name'] }, }); expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { allOf: [{ key: 'kind', values: ['b'] }, requestFilter] }, }); @@ -276,12 +277,12 @@ describe('AuthorizedEntitiesCatalog', () => { orderFieldValues: ['a', null], }; response = await catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), cursor, }); expect(fakeCatalog.queryEntities).toHaveBeenNthCalledWith(2, { - authorizationToken: 'abcd', + credentials: mockCredentials.none(), cursor: { ...cursor, filter: { allOf: [{ key: 'kind', values: ['b'] }, requestFilter] }, @@ -324,7 +325,9 @@ describe('AuthorizedEntitiesCatalog', () => { ); await expect(() => - catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }), + catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }), ).rejects.toThrow(NotAllowedError); }); @@ -343,7 +346,9 @@ describe('AuthorizedEntitiesCatalog', () => { ); await expect(() => - catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }), + catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }), ).rejects.toThrow(NotAllowedError); }); @@ -363,9 +368,13 @@ describe('AuthorizedEntitiesCatalog', () => { createConditionTransformer([isEntityKind]), ); - await catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }); + await catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }); - expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid'); + expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid', { + credentials: mockCredentials.none(), + }); }); it('calls underlying catalog method on ALLOW', async () => { @@ -383,9 +392,13 @@ describe('AuthorizedEntitiesCatalog', () => { createConditionTransformer([]), ); - await catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }); + await catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }); - expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid'); + expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid', { + credentials: mockCredentials.none(), + }); }); }); @@ -398,7 +411,7 @@ describe('AuthorizedEntitiesCatalog', () => { await expect(() => catalog.entityAncestry('backstage:default/component', { - authorizationToken: 'Bearer abcd', + credentials: mockCredentials.none(), }), ).rejects.toThrow(NotAllowedError); }); @@ -443,7 +456,7 @@ describe('AuthorizedEntitiesCatalog', () => { const ancestryResult = await catalog.entityAncestry( 'backstage:default/a', - { authorizationToken: 'Bearer abcd' }, + { credentials: mockCredentials.none() }, ); expect(ancestryResult).toEqual({ @@ -476,7 +489,7 @@ describe('AuthorizedEntitiesCatalog', () => { expect( await catalog.facets({ facets: ['a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }), ).toEqual({ facets: { a: [] }, @@ -492,11 +505,14 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(isEntityKind); - await catalog.facets({ facets: ['a'], authorizationToken: 'abcd' }); + await catalog.facets({ + facets: ['a'], + credentials: mockCredentials.none(), + }); expect(fakeCatalog.facets).toHaveBeenCalledWith({ facets: ['a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -507,11 +523,14 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(); - await catalog.facets({ facets: ['a'], authorizationToken: 'abcd' }); + await catalog.facets({ + facets: ['a'], + credentials: mockCredentials.none(), + }); expect(fakeCatalog.facets).toHaveBeenCalledWith({ facets: ['a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); }); }); diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts index f302a2b75a..5358e90825 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts @@ -20,10 +20,7 @@ import { catalogEntityReadPermission, } from '@backstage/plugin-catalog-common/alpha'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ConditionTransformer } from '@backstage/plugin-permission-node'; import { Cursor, @@ -41,19 +38,23 @@ import { import { basicEntityFilter } from './request'; import { isQueryEntitiesCursorRequest } from './util'; import { EntityFilter } from '@backstage/plugin-catalog-node'; +import { + BackstageCredentials, + PermissionsService, +} from '@backstage/backend-plugin-api'; export class AuthorizedEntitiesCatalog implements EntitiesCatalog { constructor( private readonly entitiesCatalog: EntitiesCatalog, - private readonly permissionApi: PermissionEvaluator, + private readonly permissionApi: PermissionsService, private readonly transformConditions: ConditionTransformer, ) {} - async entities(request?: EntitiesRequest): Promise { + async entities(request: EntitiesRequest): Promise { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request?.authorizationToken }, + { credentials: request.credentials }, ) )[0]; @@ -85,7 +86,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request?.authorizationToken }, + { credentials: request.credentials }, ) )[0]; @@ -116,7 +117,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request.authorizationToken }, + { credentials: request.credentials }, ) )[0]; @@ -186,12 +187,12 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { async removeEntityByUid( uid: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const authorizeResponse = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityDeletePermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (authorizeResponse.result === AuthorizeResult.DENY) { @@ -202,6 +203,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { authorizeResponse.conditions, ); const { entities } = await this.entitiesCatalog.entities({ + credentials: options.credentials, filter: { allOf: [permissionFilter, basicEntityFilter({ 'metadata.uid': uid })], }, @@ -210,30 +212,35 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { throw new NotAllowedError(); } } - return this.entitiesCatalog.removeEntityByUid(uid); + return this.entitiesCatalog.removeEntityByUid(uid, { + credentials: options.credentials, + }); } async entityAncestry( entityRef: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const rootEntityAuthorizeResponse = ( await this.permissionApi.authorize( [{ permission: catalogEntityReadPermission, resourceRef: entityRef }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (rootEntityAuthorizeResponse.result === AuthorizeResult.DENY) { throw new NotAllowedError(); } - const ancestryResult = await this.entitiesCatalog.entityAncestry(entityRef); + const ancestryResult = await this.entitiesCatalog.entityAncestry( + entityRef, + { credentials: options.credentials }, + ); const authorizeResponse = await this.permissionApi.authorize( ancestryResult.items.map(item => ({ permission: catalogEntityReadPermission, resourceRef: stringifyEntityRef(item.entity), })), - { token: options?.authorizationToken }, + { credentials: options.credentials }, ); const unauthorizedAncestryItems = ancestryResult.items.filter( (_, index) => authorizeResponse[index].result === AuthorizeResult.DENY, @@ -268,7 +275,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request?.authorizationToken }, + { credentials: request.credentials }, ) )[0]; diff --git a/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts b/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts index c2ffee8003..2eae1c7553 100644 --- a/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts @@ -17,6 +17,7 @@ import { NotAllowedError, NotFoundError } from '@backstage/errors'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { AuthorizedLocationService } from './AuthorizedLocationService'; +import { mockCredentials } from '@backstage/backend-test-utils'; describe('AuthorizedLocationService', () => { const fakeLocationService = { @@ -45,6 +46,10 @@ describe('AuthorizedLocationService', () => { const createService = () => new AuthorizedLocationService(fakeLocationService, fakePermissionApi); + const mockOptions = { + credentials: mockCredentials.none(), + }; + afterEach(() => { jest.resetAllMocks(); }); @@ -55,13 +60,12 @@ describe('AuthorizedLocationService', () => { const service = createService(); const spec = { type: 'type', target: 'target' }; - await service.createLocation(spec, false, { - authorizationToken: 'Bearer authtoken', - }); + await service.createLocation(spec, false, mockOptions); expect(fakeLocationService.createLocation).toHaveBeenCalledWith( spec, false, + mockOptions, ); }); @@ -71,9 +75,7 @@ describe('AuthorizedLocationService', () => { const spec = { type: 'type', target: 'target' }; await expect(() => - service.createLocation(spec, false, { - authorizationToken: 'Bearer authtoken', - }), + service.createLocation(spec, false, mockOptions), ).rejects.toThrow(NotAllowedError); }); }); @@ -83,7 +85,7 @@ describe('AuthorizedLocationService', () => { mockAllow(); const service = createService(); - await service.listLocations({ authorizationToken: 'Bearer authtoken' }); + await service.listLocations(mockOptions); expect(fakeLocationService.listLocations).toHaveBeenCalled(); }); @@ -92,9 +94,7 @@ describe('AuthorizedLocationService', () => { mockDeny(); const service = createService(); - const locations = await service.listLocations({ - authorizationToken: 'Bearer authtoken', - }); + const locations = await service.listLocations(mockOptions); expect(locations).toEqual([]); }); @@ -105,11 +105,12 @@ describe('AuthorizedLocationService', () => { mockAllow(); const service = createService(); - await service.getLocation('id', { - authorizationToken: 'Bearer authtoken', - }); + await service.getLocation('id', mockOptions); - expect(fakeLocationService.getLocation).toHaveBeenCalledWith('id'); + expect(fakeLocationService.getLocation).toHaveBeenCalledWith( + 'id', + mockOptions, + ); }); it('throws error on DENY', async () => { @@ -117,7 +118,7 @@ describe('AuthorizedLocationService', () => { const service = createService(); await expect(() => - service.getLocation('id', { authorizationToken: 'Bearer authtoken' }), + service.getLocation('id', mockOptions), ).rejects.toThrow(NotFoundError); }); }); @@ -127,11 +128,12 @@ describe('AuthorizedLocationService', () => { mockAllow(); const service = createService(); - await service.deleteLocation('id', { - authorizationToken: 'Bearer authtoken', - }); + await service.deleteLocation('id', mockOptions); - expect(fakeLocationService.deleteLocation).toHaveBeenCalledWith('id'); + expect(fakeLocationService.deleteLocation).toHaveBeenCalledWith( + 'id', + mockOptions, + ); }); it('throws error on DENY', async () => { @@ -139,9 +141,7 @@ describe('AuthorizedLocationService', () => { const service = createService(); await expect(() => - service.deleteLocation('id', { - authorizationToken: 'Bearer authtoken', - }), + service.deleteLocation('id', mockOptions), ).rejects.toThrow(NotAllowedError); }); }); @@ -153,16 +153,17 @@ describe('AuthorizedLocationService', () => { await service.getLocationByEntity( { kind: 'c', namespace: 'ns', name: 'n' }, - { - authorizationToken: 'Bearer authtoken', - }, + mockOptions, ); - expect(fakeLocationService.getLocationByEntity).toHaveBeenCalledWith({ - kind: 'c', - namespace: 'ns', - name: 'n', - }); + expect(fakeLocationService.getLocationByEntity).toHaveBeenCalledWith( + { + kind: 'c', + namespace: 'ns', + name: 'n', + }, + mockOptions, + ); }); it('throws error on DENY', async () => { @@ -172,7 +173,7 @@ describe('AuthorizedLocationService', () => { await expect(() => service.getLocationByEntity( { kind: 'c', namespace: 'ns', name: 'n' }, - { authorizationToken: 'Bearer authtoken' }, + mockOptions, ), ).rejects.toThrow(NotFoundError); }); diff --git a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts index 0b9d50b20e..3eb3eede12 100644 --- a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts @@ -22,23 +22,24 @@ import { catalogLocationDeletePermission, catalogLocationReadPermission, } from '@backstage/plugin-catalog-common/alpha'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { LocationInput, LocationService } from './types'; +import { + BackstageCredentials, + PermissionsService, +} from '@backstage/backend-plugin-api'; export class AuthorizedLocationService implements LocationService { constructor( private readonly locationService: LocationService, - private readonly permissionApi: PermissionEvaluator, + private readonly permissionApi: PermissionsService, ) {} async createLocation( spec: LocationInput, dryRun: boolean, - options?: { - authorizationToken?: string; + options: { + credentials: BackstageCredentials; }, ): Promise<{ location: Location; @@ -48,7 +49,7 @@ export class AuthorizedLocationService implements LocationService { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationCreatePermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -56,16 +57,16 @@ export class AuthorizedLocationService implements LocationService { throw new NotAllowedError(); } - return this.locationService.createLocation(spec, dryRun); + return this.locationService.createLocation(spec, dryRun, options); } - async listLocations(options?: { - authorizationToken?: string; + async listLocations(options: { + credentials: BackstageCredentials; }): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationReadPermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -73,17 +74,17 @@ export class AuthorizedLocationService implements LocationService { return []; } - return this.locationService.listLocations(); + return this.locationService.listLocations(options); } async getLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationReadPermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -91,17 +92,17 @@ export class AuthorizedLocationService implements LocationService { throw new NotFoundError(`Found no location with ID ${id}`); } - return this.locationService.getLocation(id); + return this.locationService.getLocation(id, options); } async deleteLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationDeletePermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -109,23 +110,23 @@ export class AuthorizedLocationService implements LocationService { throw new NotAllowedError(); } - return this.locationService.deleteLocation(id); + return this.locationService.deleteLocation(id, options); } async getLocationByEntity( entityRef: CompoundEntityRef | string, - options?: { authorizationToken?: string | undefined } | undefined, + options: { credentials: BackstageCredentials }, ): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationReadPermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (authorizationResponse.result === AuthorizeResult.DENY) { throw new NotFoundError(); } - return this.locationService.getLocationByEntity(entityRef); + return this.locationService.getLocationByEntity(entityRef, options); } } diff --git a/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts b/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts index f37bd0ec49..ba9ef2db73 100644 --- a/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts @@ -18,6 +18,7 @@ import { NotAllowedError } from '@backstage/errors'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { AuthorizedRefreshService } from './AuthorizedRefreshService'; +import { mockCredentials } from '@backstage/backend-test-utils'; describe('AuthorizedRefreshService', () => { const refreshService = { @@ -46,7 +47,7 @@ describe('AuthorizedRefreshService', () => { await expect(() => authorizedService.refresh({ entityRef: 'some entity ref', - authorizationToken: 'some auth token', + credentials: mockCredentials.none(), }), ).rejects.toThrow(NotAllowedError); }); @@ -64,7 +65,7 @@ describe('AuthorizedRefreshService', () => { const options = { entityRef: 'some entity ref', - authorizationToken: 'some auth token', + credentials: mockCredentials.none(), }; await authorizedService.refresh(options); diff --git a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts index cbc728750a..a5a2491a04 100644 --- a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts @@ -16,16 +16,14 @@ import { NotAllowedError } from '@backstage/errors'; import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { RefreshOptions, RefreshService } from './types'; +import { PermissionsService } from '@backstage/backend-plugin-api'; export class AuthorizedRefreshService implements RefreshService { constructor( private readonly service: RefreshService, - private readonly permissionApi: PermissionEvaluator, + private readonly permissionApi: PermissionsService, ) {} async refresh(options: RefreshOptions) { @@ -37,7 +35,7 @@ export class AuthorizedRefreshService implements RefreshService { resourceRef: options.entityRef, }, ], - { token: options.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (authorizeDecision.result !== AuthorizeResult.ALLOW) { diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 0abaf2ec0a..7fcee231fd 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common'; +import { + PluginDatabaseManager, + HostDiscovery, + UrlReader, + createLegacyAuthAdapters, +} from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { DefaultNamespaceEntityPolicy, @@ -84,7 +89,6 @@ import { permissionRules as catalogPermissionRules } from '../permissions/rules' import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionAuthorizer, - PermissionEvaluator, toPermissionEvaluator, } from '@backstage/plugin-permission-common'; import { @@ -102,6 +106,12 @@ import { DefaultProviderDatabase } from '../database/DefaultProviderDatabase'; import { DefaultCatalogDatabase } from '../database/DefaultCatalogDatabase'; import { EventBroker } from '@backstage/plugin-events-node'; import { durationToMilliseconds } from '@backstage/types'; +import { + DiscoveryService, + AuthService, + HttpAuthService, + PermissionsService, +} from '@backstage/backend-plugin-api'; /** * This is a duplicate of the alpha `CatalogPermissionRule` type, for use in the stable API. @@ -118,8 +128,11 @@ export type CatalogEnvironment = { database: PluginDatabaseManager; config: Config; reader: UrlReader; - permissions: PermissionEvaluator | PermissionAuthorizer; + permissions: PermissionsService | PermissionAuthorizer; scheduler?: PluginTaskScheduler; + discovery?: DiscoveryService; + auth?: AuthService; + httpAuth?: HttpAuthService; }; /** @@ -438,7 +451,19 @@ export class CatalogBuilder { processingEngine: CatalogProcessingEngine; router: Router; }> { - const { config, database, logger, permissions, scheduler } = this.env; + const { + config, + database, + logger, + permissions, + scheduler, + discovery = HostDiscovery.fromConfig(config), + } = this.env; + + const { auth, httpAuth } = createLegacyAuthAdapters({ + ...this.env, + discovery, + }); const policy = this.buildEntityPolicy(); const processors = this.buildProcessors(); @@ -486,25 +511,26 @@ export class CatalogBuilder { stitcher, }); - let permissionEvaluator: PermissionEvaluator; + let permissionsService: PermissionsService; if ('authorizeConditional' in permissions) { - permissionEvaluator = permissions as PermissionEvaluator; + permissionsService = permissions as PermissionsService; } else { logger.warn( 'PermissionAuthorizer is deprecated. Please use an instance of PermissionEvaluator instead of PermissionAuthorizer in PluginEnvironment#permissions', ); - permissionEvaluator = toPermissionEvaluator(permissions); + permissionsService = toPermissionEvaluator(permissions); } const entitiesCatalog = new AuthorizedEntitiesCatalog( unauthorizedEntitiesCatalog, - permissionEvaluator, + permissionsService, createConditionTransformer(this.permissionRules), ); const permissionIntegrationRouter = createPermissionIntegrationRouter({ resourceType: RESOURCE_TYPE_CATALOG_ENTITY, getResources: async (resourceRefs: string[]) => { const { entities } = await unauthorizedEntitiesCatalog.entities({ + credentials: await auth.getOwnServiceCredentials(), filter: { anyOf: resourceRefs.map(resourceRef => { const { kind, namespace, name } = parseEntityRef(resourceRef); @@ -558,12 +584,13 @@ export class CatalogBuilder { new DefaultLocationService(locationStore, orchestrator, { allowedLocationTypes: this.allowedLocationType, }), - permissionEvaluator, + permissionsService, ); const refreshService = new AuthorizedRefreshService( new DefaultRefreshService({ database: catalogDatabase }), - permissionEvaluator, + permissionsService, ); + const router = await createRouter({ entitiesCatalog, locationAnalyzer, @@ -573,6 +600,8 @@ export class CatalogBuilder { logger, config, permissionIntegrationRouter, + auth, + httpAuth, }); await connectEntityProviders(providerDatabase, entityProviders); diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 255df84d58..6e4424922d 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -31,6 +31,7 @@ import { } from '@backstage/plugin-catalog-node/alpha'; import { CatalogProcessor, + CatalogProcessorParser, EntityProvider, PlaceholderResolver, ScmLocationAnalyzer, @@ -137,6 +138,21 @@ class CatalogModelExtensionPointImpl implements CatalogModelExtensionPoint { get fieldValidators() { return this.#fieldValidators; } + + #entityDataParser?: CatalogProcessorParser; + + setEntityDataParser(parser: CatalogProcessorParser): void { + if (this.#entityDataParser) { + throw new Error( + 'Attempted to install second EntityDataParser. Only one can be set.', + ); + } + this.#entityDataParser = parser; + } + + get entityDataParser() { + return this.#entityDataParser; + } } /** @@ -178,6 +194,9 @@ export const catalogPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, lifecycle: coreServices.lifecycle, scheduler: coreServices.scheduler, + discovery: coreServices.discovery, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, }, async init({ logger, @@ -188,6 +207,9 @@ export const catalogPlugin = createBackendPlugin({ httpRouter, lifecycle, scheduler, + discovery, + auth, + httpAuth, }) { const winstonLogger = loggerToWinstonLogger(logger); const builder = await CatalogBuilder.create({ @@ -197,6 +219,9 @@ export const catalogPlugin = createBackendPlugin({ database, scheduler, logger: winstonLogger, + discovery, + auth, + httpAuth, }); if (processingExtensions.onProcessingErrorHandler) { builder.subscribe({ @@ -205,6 +230,11 @@ export const catalogPlugin = createBackendPlugin({ } builder.addProcessor(...processingExtensions.processors); builder.addEntityProvider(...processingExtensions.entityProviders); + + if (modelExtensions.entityDataParser) { + builder.setEntityDataParser(modelExtensions.entityDataParser); + } + Object.entries(processingExtensions.placeholderResolvers).forEach( ([key, resolver]) => builder.setPlaceholderResolver(key, resolver), ); diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index fe7d57c89a..4d8dc7c7a4 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -15,7 +15,11 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + TestDatabaseId, + TestDatabases, + mockCredentials, +} from '@backstage/backend-test-utils'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { Knex } from 'knex'; import { v4 as uuid, v4 } from 'uuid'; @@ -304,8 +308,10 @@ describe('DefaultEntitiesCatalog', () => { const testFilter = { key: 'spec.test', }; - const request = { filter: testFilter }; - const { entities } = await catalog.entities(request); + const { entities } = await catalog.entities({ + filter: testFilter, + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(1); expect(entities[0]).toEqual(entity2); @@ -343,8 +349,10 @@ describe('DefaultEntitiesCatalog', () => { key: 'spec.test', }, }; - const request = { filter: testFilter }; - const { entities } = await catalog.entities(request); + const { entities } = await catalog.entities({ + filter: testFilter, + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(1); expect(entities[0]).toEqual(entity1); @@ -406,7 +414,7 @@ describe('DefaultEntitiesCatalog', () => { values: ['red'], }, }; - const request = { + const { entities } = await catalog.entities({ filter: { allOf: [ testFilter1, @@ -415,8 +423,8 @@ describe('DefaultEntitiesCatalog', () => { }, ], }, - }; - const { entities } = await catalog.entities(request); + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(2); expect(entities).toContainEqual(entity2); @@ -455,14 +463,15 @@ describe('DefaultEntitiesCatalog', () => { const testFilter2 = { key: 'metadata.desc', }; - const request = { + const { entities } = await catalog.entities({ filter: { not: { allOf: [testFilter1, testFilter2], }, }, - }; - const { entities } = await catalog.entities(request); + + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(1); expect(entities).toContainEqual(entity1); @@ -498,8 +507,10 @@ describe('DefaultEntitiesCatalog', () => { key: 'kind', values: [], }; - const request = { filter: testFilter }; - const { entities } = await catalog.entities(request); + const { entities } = await catalog.entities({ + filter: testFilter, + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(0); }, @@ -603,9 +614,11 @@ describe('DefaultEntitiesCatalog', () => { stitcher, }); - function f(request: EntitiesRequest): Promise { + function f( + request: Omit, + ): Promise { return catalog - .entities(request) + .entities({ ...request, credentials: mockCredentials.none() }) .then(response => response.entities.map(e => e.metadata.name)); } @@ -701,6 +714,7 @@ describe('DefaultEntitiesCatalog', () => { 'k:default/does-not-exist', 'k:default/two', ], + credentials: mockCredentials.none(), }); expect(items.map(e => e && stringifyEntityRef(e))).toEqual([ @@ -749,6 +763,7 @@ describe('DefaultEntitiesCatalog', () => { const { items } = await catalog.entitiesBatch({ entityRefs: ['k:default/two', 'k:default/one'], filter: { key: 'spec.owner', values: ['me'] }, + credentials: mockCredentials.none(), }); expect(items.map(e => e && stringifyEntityRef(e))).toEqual([ @@ -804,6 +819,7 @@ describe('DefaultEntitiesCatalog', () => { filter, limit, orderFields: [{ field: 'metadata.name', order: 'asc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toEqual([entityFrom('A'), entityFrom('B')]); @@ -815,6 +831,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toEqual([entityFrom('C'), entityFrom('D')]); @@ -826,6 +843,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toEqual([entityFrom('E'), entityFrom('F')]); @@ -837,6 +855,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); expect(response4.items).toEqual([entityFrom('C'), entityFrom('D')]); @@ -848,6 +867,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toEqual([entityFrom('A'), entityFrom('B')]); @@ -859,6 +879,7 @@ describe('DefaultEntitiesCatalog', () => { const request6: QueryEntitiesCursorRequest = { cursor: response5.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response6 = await catalog.queryEntities(request6); expect(response6.items).toEqual([entityFrom('C'), entityFrom('D')]); @@ -870,6 +891,7 @@ describe('DefaultEntitiesCatalog', () => { const request7: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response7 = await catalog.queryEntities(request7); expect(response7.items).toEqual([entityFrom('E'), entityFrom('F')]); @@ -881,6 +903,7 @@ describe('DefaultEntitiesCatalog', () => { const request7bis: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit: limit + 1, + credentials: mockCredentials.none(), }; const response7bis = await catalog.queryEntities(request7bis); expect(response7bis.items).toEqual([ @@ -896,6 +919,7 @@ describe('DefaultEntitiesCatalog', () => { const request8: QueryEntitiesCursorRequest = { cursor: response7.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response8 = await catalog.queryEntities(request8); expect(response8.items).toEqual([entityFrom('G')]); @@ -949,6 +973,7 @@ describe('DefaultEntitiesCatalog', () => { filter, limit, orderFields: [{ field: 'metadata.name', order: 'desc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toEqual([entityFrom('G'), entityFrom('F')]); @@ -960,6 +985,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toEqual([entityFrom('E'), entityFrom('D')]); @@ -971,6 +997,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toEqual([entityFrom('C'), entityFrom('B')]); @@ -982,6 +1009,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); @@ -994,6 +1022,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toEqual([entityFrom('G'), entityFrom('F')]); @@ -1005,6 +1034,7 @@ describe('DefaultEntitiesCatalog', () => { const request6: QueryEntitiesCursorRequest = { cursor: response5.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response6 = await catalog.queryEntities(request6); expect(response6.items).toEqual([entityFrom('E'), entityFrom('D')]); @@ -1016,6 +1046,7 @@ describe('DefaultEntitiesCatalog', () => { const request7: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response7 = await catalog.queryEntities(request7); expect(response7.items).toEqual([entityFrom('C'), entityFrom('B')]); @@ -1027,6 +1058,7 @@ describe('DefaultEntitiesCatalog', () => { const request7bis: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit: limit + 1, + credentials: mockCredentials.none(), }; const response7bis = await catalog.queryEntities(request7bis); expect(response7bis.items).toEqual([ @@ -1042,6 +1074,7 @@ describe('DefaultEntitiesCatalog', () => { const request8: QueryEntitiesCursorRequest = { cursor: response7.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response8 = await catalog.queryEntities(request8); expect(response8.items).toEqual([entityFrom('A')]); @@ -1094,6 +1127,7 @@ describe('DefaultEntitiesCatalog', () => { orderFields: [{ field: 'metadata.name', order: 'asc' }], fullTextFilter: { term: 'cAt ' }, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); expect(response.items).toEqual([ @@ -1152,6 +1186,7 @@ describe('DefaultEntitiesCatalog', () => { filter, limit: 100, fullTextFilter: { term: 'cAt ', fields: ['metadata.title'] }, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); expect(response.items).toEqual([ @@ -1177,6 +1212,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponseNext = await catalog.queryEntities({ cursor: paginatedResponse.pageInfo.nextCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponseNext.items).toEqual([ entityFrom('4', { uid: 'id4', title: 'dogcat' }), @@ -1187,6 +1223,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponsePrev = await catalog.queryEntities({ cursor: paginatedResponseNext.pageInfo.prevCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponsePrev).toMatchObject(paginatedResponse); }, @@ -1251,6 +1288,7 @@ describe('DefaultEntitiesCatalog', () => { term: 'KiNg ', fields: ['metadata.title', 'metadata.name'], }, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); @@ -1278,6 +1316,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponseNext = await catalog.queryEntities({ cursor: paginatedResponse.pageInfo.nextCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponseNext.items).toEqual([ entityFrom('NotACatKing', { uid: 'id2', title: 'atcatss' }), @@ -1289,6 +1328,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponsePrev = await catalog.queryEntities({ cursor: paginatedResponseNext.pageInfo.prevCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponsePrev).toMatchObject(paginatedResponse); }, @@ -1319,6 +1359,7 @@ describe('DefaultEntitiesCatalog', () => { const request: QueryEntitiesInitialRequest = { limit: 0, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); expect(response).toEqual({ totalItems: 20, items: [], pageInfo: {} }); @@ -1351,6 +1392,7 @@ describe('DefaultEntitiesCatalog', () => { const request1: QueryEntitiesInitialRequest = { limit, orderFields: [{ field: 'metadata.name', order: 'asc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toMatchObject([ @@ -1365,6 +1407,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toMatchObject([ @@ -1379,6 +1422,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toEqual([entityFrom('CC'), entityFrom('DD')]); @@ -1390,6 +1434,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); expect(response4.items).toMatchObject([ @@ -1404,6 +1449,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toMatchObject([ @@ -1471,6 +1517,7 @@ describe('DefaultEntitiesCatalog', () => { values: ['included'], }, orderFields: [{ field: 'metadata.name', order: 'asc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toMatchObject([ @@ -1485,6 +1532,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toMatchObject([ @@ -1528,6 +1576,7 @@ describe('DefaultEntitiesCatalog', () => { // initial request const request1: QueryEntitiesInitialRequest = { limit, + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toMatchObject([ @@ -1542,6 +1591,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toMatchObject([ @@ -1556,6 +1606,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toMatchObject([ @@ -1570,6 +1621,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); expect(response4.items).toMatchObject([ @@ -1584,6 +1636,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toMatchObject([ @@ -1719,7 +1772,12 @@ describe('DefaultEntitiesCatalog', () => { stitcher, }); - await expect(catalog.facets({ facets: ['kind'] })).resolves.toEqual({ + await expect( + catalog.facets({ + facets: ['kind'], + credentials: mockCredentials.none(), + }), + ).resolves.toEqual({ facets: { kind: [ { value: 'k', count: 2 }, @@ -1732,6 +1790,7 @@ describe('DefaultEntitiesCatalog', () => { catalog.facets({ facets: ['kind'], filter: { not: { key: 'metadata.name', values: ['two'] } }, + credentials: mockCredentials.none(), }), ).resolves.toEqual({ facets: { @@ -1775,6 +1834,7 @@ describe('DefaultEntitiesCatalog', () => { await expect( catalog.facets({ facets: ['metadata.annotations.a.b/c.d', 'metadata.labels.e.f/g.h'], + credentials: mockCredentials.none(), }), ).resolves.toEqual({ facets: { @@ -1823,6 +1883,7 @@ describe('DefaultEntitiesCatalog', () => { await expect( catalog.facets({ facets: ['metadata.tags'], + credentials: mockCredentials.none(), }), ).resolves.toEqual({ facets: { diff --git a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts index e1b775cce6..9ca9ae0a5f 100644 --- a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts @@ -15,7 +15,11 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + TestDatabaseId, + TestDatabases, + mockCredentials, +} from '@backstage/backend-test-utils'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { createHash } from 'crypto'; import { Knex } from 'knex'; @@ -220,6 +224,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'component:default/mycomp', + credentials: mockCredentials.none(), }); await expect( @@ -273,6 +278,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'api:default/myapi', + credentials: mockCredentials.none(), }); await expect(waitForRefresh(knex, 'api:default/myapi')).resolves.toBe( @@ -324,6 +330,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'component:default/mycomp', + credentials: mockCredentials.none(), }); await expect( @@ -334,6 +341,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'component:default/mycomp', + credentials: mockCredentials.none(), }); await expect( diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 3eb00c5bd6..5177f3b42b 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -41,6 +41,7 @@ import { z } from 'zod'; import { decodeCursor, encodeCursor } from './util'; import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils'; import { Server } from 'http'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; describe('createRouter readonly disabled', () => { let entitiesCatalog: jest.Mocked; @@ -75,6 +76,8 @@ describe('createRouter readonly disabled', () => { refreshService, config: new ConfigReader(undefined), permissionIntegrationRouter: express.Router(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = wrapInOpenApiTestServer(express().use(router)); }); @@ -88,15 +91,30 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/refresh') .set('Content-Type', 'application/json') - .set('authorization', 'Bearer someauthtoken') .send({ entityRef: 'Component/default:foo' }); expect(response.status).toBe(200); expect(refreshService.refresh).toHaveBeenCalledWith({ entityRef: 'Component/default:foo', - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), + }); + }); + + it('should support passing the token in the request body for backwards compatibility', async () => { + const response = await request(app) + .post('/refresh') + .set('Content-Type', 'application/json') + .send({ + entityRef: 'Component/default:foo', + authorizationToken: mockCredentials.user.token('user:default/other'), + }); + expect(response.status).toBe(200); + expect(refreshService.refresh).toHaveBeenCalledWith({ + entityRef: 'Component/default:foo', + credentials: mockCredentials.user('user:default/other'), }); }); }); + describe('GET /entities', () => { it('happy path: lists entities', async () => { const entities: Entity[] = [ @@ -137,6 +155,7 @@ describe('createRouter readonly disabled', () => { { allOf: [{ key: 'c', values: ['4'] }] }, ], }, + credentials: mockCredentials.user(), }); }); }); @@ -196,6 +215,7 @@ describe('createRouter readonly disabled', () => { fields: undefined, term: '', }, + credentials: mockCredentials.user(), }); }); @@ -235,6 +255,7 @@ describe('createRouter readonly disabled', () => { fields: undefined, term: '', }, + credentials: mockCredentials.user(), }); }); @@ -257,6 +278,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.queryEntities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith({ cursor, + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual({ @@ -291,6 +313,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.queryEntities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith({ cursor, + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual({ @@ -370,6 +393,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.entities).toHaveBeenCalledWith({ filter: basicEntityFilter({ 'metadata.uid': 'zzz' }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); @@ -386,6 +410,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.entities).toHaveBeenCalledWith({ filter: basicEntityFilter({ 'metadata.uid': 'zzz' }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(404); expect(response.text).toMatch(/uid/); @@ -416,6 +441,7 @@ describe('createRouter readonly disabled', () => { 'metadata.namespace': 'ns', 'metadata.name': 'n', }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); @@ -436,6 +462,7 @@ describe('createRouter readonly disabled', () => { 'metadata.namespace': 'd', 'metadata.name': 'c', }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(404); expect(response.text).toMatch(/name/); @@ -446,13 +473,10 @@ describe('createRouter readonly disabled', () => { it('can remove', async () => { entitiesCatalog.removeEntityByUid.mockResolvedValue(undefined); - const response = await request(app) - .delete('/entities/by-uid/apa') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/entities/by-uid/apa'); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(204); }); @@ -462,13 +486,10 @@ describe('createRouter readonly disabled', () => { new NotFoundError('nope'), ); - const response = await request(app) - .delete('/entities/by-uid/apa') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/entities/by-uid/apa'); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(404); }); @@ -518,6 +539,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.entitiesBatch).toHaveBeenCalledWith({ entityRefs: [entityRef], fields: expect.any(Function), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual({ items: [entity] }); @@ -531,13 +553,10 @@ describe('createRouter readonly disabled', () => { ]; locationService.listLocations.mockResolvedValueOnce(locations); - const response = await request(app) - .get('/locations') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations'); expect(locationService.listLocations).toHaveBeenCalledTimes(1); expect(locationService.listLocations).toHaveBeenCalledWith({ - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual([ @@ -555,13 +574,10 @@ describe('createRouter readonly disabled', () => { }; locationService.getLocation.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/foo'); expect(locationService.getLocation).toHaveBeenCalledTimes(1); expect(locationService.getLocation).toHaveBeenCalledWith('foo', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); @@ -582,7 +598,7 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/locations') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).not.toHaveBeenCalled(); @@ -602,12 +618,12 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/locations') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).toHaveBeenCalledTimes(1); expect(locationService.createLocation).toHaveBeenCalledWith(spec, false, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(201); expect(response.body).toEqual( @@ -630,12 +646,12 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/locations?dryRun=true') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).toHaveBeenCalledTimes(1); expect(locationService.createLocation).toHaveBeenCalledWith(spec, true, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(201); expect(response.body).toEqual( @@ -650,13 +666,10 @@ describe('createRouter readonly disabled', () => { it('deletes the location', async () => { locationService.deleteLocation.mockResolvedValueOnce(undefined); - const response = await request(app) - .delete('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/locations/foo'); expect(locationService.deleteLocation).toHaveBeenCalledTimes(1); expect(locationService.deleteLocation).toHaveBeenCalledWith('foo', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(204); @@ -672,15 +685,12 @@ describe('createRouter readonly disabled', () => { }; locationService.getLocationByEntity.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/by-entity/c/ns/n') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/by-entity/c/ns/n'); expect(locationService.getLocationByEntity).toHaveBeenCalledTimes(1); expect(locationService.getLocationByEntity).toHaveBeenCalledWith( { kind: 'c', namespace: 'ns', name: 'n' }, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }, ); @@ -837,6 +847,8 @@ describe('createRouter readonly enabled', () => { }, }), permissionIntegrationRouter: express.Router(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = express().use(router); }); @@ -866,13 +878,10 @@ describe('createRouter readonly enabled', () => { describe('DELETE /entities/by-uid/:uid', () => { // this delete is allowed as there is no other way to remove entities it('is allowed', async () => { - const response = await request(app) - .delete('/entities/by-uid/apa') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/entities/by-uid/apa'); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(204); }); @@ -885,13 +894,10 @@ describe('createRouter readonly enabled', () => { ]; locationService.listLocations.mockResolvedValueOnce(locations); - const response = await request(app) - .get('/locations') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations'); expect(locationService.listLocations).toHaveBeenCalledTimes(1); expect(locationService.listLocations).toHaveBeenCalledWith({ - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); @@ -910,13 +916,10 @@ describe('createRouter readonly enabled', () => { }; locationService.getLocation.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/foo'); expect(locationService.getLocation).toHaveBeenCalledTimes(1); expect(locationService.getLocation).toHaveBeenCalledWith('foo', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); @@ -937,7 +940,7 @@ describe('createRouter readonly enabled', () => { const response = await request(app) .post('/locations') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).not.toHaveBeenCalled(); @@ -958,12 +961,12 @@ describe('createRouter readonly enabled', () => { const response = await request(app) .post('/locations?dryRun=true') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).toHaveBeenCalledTimes(1); expect(locationService.createLocation).toHaveBeenCalledWith(spec, true, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(201); expect(response.body).toEqual( @@ -976,10 +979,7 @@ describe('createRouter readonly enabled', () => { describe('DELETE /locations', () => { it('is not allowed', async () => { - const response = await request(app) - .delete('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/locations/foo'); expect(locationService.deleteLocation).not.toHaveBeenCalled(); expect(response.status).toEqual(403); }); @@ -994,15 +994,12 @@ describe('createRouter readonly enabled', () => { }; locationService.getLocationByEntity.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/by-entity/c/ns/n') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/by-entity/c/ns/n'); expect(locationService.getLocationByEntity).toHaveBeenCalledTimes(1); expect(locationService.getLocationByEntity).toHaveBeenCalledWith( { kind: 'c', namespace: 'ns', name: 'n' }, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }, ); @@ -1065,6 +1062,8 @@ describe('NextRouter permissioning', () => { ), ), }), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = express().use(router); }); diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 2522966408..922782d75a 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -41,7 +41,7 @@ import { } from './request'; import { parseEntityFacetParams } from './request/parseEntityFacetParams'; import { parseEntityOrderParams } from './request/parseEntityOrderParams'; -import { LocationService, RefreshOptions, RefreshService } from './types'; +import { LocationService, RefreshService } from './types'; import { disallowReadonlyMode, encodeCursor, @@ -50,8 +50,8 @@ import { } from './util'; import { createOpenApiRouter } from '../schema/openapi.generated'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { parseEntityPaginationParams } from './request/parseEntityPaginationParams'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; /** * Options used by {@link createRouter}. @@ -68,6 +68,8 @@ export interface RouterOptions { logger: Logger; config: Config; permissionIntegrationRouter?: express.Router; + auth: AuthService; + httpAuth: HttpAuthService; } /** @@ -94,6 +96,8 @@ export async function createRouter( config, logger, permissionIntegrationRouter, + auth, + httpAuth, } = options; const readonlyEnabled = @@ -104,12 +108,16 @@ export async function createRouter( if (refreshService) { router.post('/refresh', async (req, res) => { - const refreshOptions: RefreshOptions = req.body; - refreshOptions.authorizationToken = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); + const { authorizationToken, ...restBody } = req.body; - await refreshService.refresh(refreshOptions); + const credentials = authorizationToken + ? await auth.authenticate(authorizationToken) + : await httpAuth.credentials(req); + + await refreshService.refresh({ + ...restBody, + credentials, + }); res.status(200).end(); }); } @@ -126,9 +134,7 @@ export async function createRouter( fields: parseEntityTransformParams(req.query), order: parseEntityOrderParams(req.query), pagination: parseEntityPaginationParams(req.query), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); // Add a Link header to the next page @@ -147,9 +153,7 @@ export async function createRouter( await entitiesCatalog.queryEntities({ limit: req.query.limit, ...parseQueryEntitiesParams(req.query), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.json({ @@ -169,9 +173,7 @@ export async function createRouter( const { uid } = req.params; const { entities } = await entitiesCatalog.entities({ filter: basicEntityFilter({ 'metadata.uid': uid }), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); if (!entities.length) { throw new NotFoundError(`No entity with uid ${uid}`); @@ -181,9 +183,7 @@ export async function createRouter( .delete('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; await entitiesCatalog.removeEntityByUid(uid, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(204).end(); }) @@ -195,9 +195,7 @@ export async function createRouter( 'metadata.namespace': namespace, 'metadata.name': name, }), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); if (!entities.length) { throw new NotFoundError( @@ -212,22 +210,17 @@ export async function createRouter( const { kind, namespace, name } = req.params; const entityRef = stringifyEntityRef({ kind, namespace, name }); const response = await entitiesCatalog.entityAncestry(entityRef, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(response); }, ) .post('/entities/by-refs', async (req, res) => { const request = entitiesBatchRequest(req); - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); const response = await entitiesCatalog.entitiesBatch({ entityRefs: request.entityRefs, fields: parseEntityTransformParams(req.query, request.fields), - authorizationToken: token, + credentials: await httpAuth.credentials(req), }); res.status(200).json(response); }) @@ -235,9 +228,7 @@ export async function createRouter( const response = await entitiesCatalog.facets({ filter: parseEntityFilterParams(req.query), facets: parseEntityFacetParams(req.query), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(response); }); @@ -256,17 +247,13 @@ export async function createRouter( } const output = await locationService.createLocation(location, dryRun, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(201).json(output); }) .get('/locations', async (req, res) => { const locations = await locationService.listLocations({ - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(locations.map(l => ({ data: l }))); }) @@ -274,9 +261,7 @@ export async function createRouter( .get('/locations/:id', async (req, res) => { const { id } = req.params; const output = await locationService.getLocation(id, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(output); }) @@ -285,9 +270,7 @@ export async function createRouter( const { id } = req.params; await locationService.deleteLocation(id, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(204).end(); }) @@ -295,11 +278,7 @@ export async function createRouter( const { kind, namespace, name } = req.params; const output = await locationService.getLocationByEntity( { kind, namespace, name }, - { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), - }, + { credentials: await httpAuth.credentials(req) }, ); res.status(200).json(output); }); diff --git a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts index b1d5e8dd1f..919e97a1f7 100644 --- a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts +++ b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts @@ -28,12 +28,12 @@ import { internal } from '@backstage/backend-openapi-utils'; export function parseQueryEntitiesParams( params: internal.QuerySchema, -): Omit { +): Omit { const fields = parseEntityTransformParams(params); if (params.cursor) { const decodedCursor = decodeCursor(params.cursor); - const response: Omit = { + const response: Omit = { cursor: decodedCursor, fields, }; @@ -43,7 +43,7 @@ export function parseQueryEntitiesParams( const filter = parseEntityFilterParams(params); const orderFields = parseEntityOrderFieldParams(params); - const response: Omit = { + const response: Omit = { fields, filter, orderFields, diff --git a/plugins/catalog-backend/src/service/types.ts b/plugins/catalog-backend/src/service/types.ts index 878afe5662..30470e5af0 100644 --- a/plugins/catalog-backend/src/service/types.ts +++ b/plugins/catalog-backend/src/service/types.ts @@ -16,6 +16,7 @@ import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; import { Location } from '@backstage/catalog-client'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; /** * Holds the information required to create a new location in the catalog location store. @@ -35,22 +36,24 @@ export interface LocationService { createLocation( location: LocationInput, dryRun: boolean, - options?: { - authorizationToken?: string; + options: { + credentials: BackstageCredentials; }, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }>; - listLocations(options?: { authorizationToken?: string }): Promise; + listLocations(options: { + credentials: BackstageCredentials; + }): Promise; getLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; deleteLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; getLocationByEntity( entityRef: CompoundEntityRef | string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; } @@ -62,7 +65,7 @@ export interface LocationService { export type RefreshOptions = { /** The reference to a single entity that should be refreshed */ entityRef: string; - authorizationToken?: string; + credentials: BackstageCredentials; }; /** diff --git a/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx b/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx index 38372e59ba..cf5427517a 100644 --- a/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx +++ b/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx @@ -22,7 +22,7 @@ import { EntityListComponent } from '../EntityListComponent'; import { PrepareResult } from '../useImportState'; import { Link } from '@backstage/core-components'; import partition from 'lodash/partition'; -import { CompoundEntityRef } from '@backstage/catalog-model'; +import { CompoundEntityRef, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { useRouteRef } from '@backstage/core-plugin-api'; @@ -46,7 +46,12 @@ const filterComponentEntity = ( entity.kind.toLocaleLowerCase('en-US'), ) ) { - return entity; + return { + kind: entity.kind.toLocaleLowerCase('en-US'), + namespace: + entity.namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE, + name: entity.name, + }; } } } diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index e43d1bd49f..17ad6e7c1e 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -5,6 +5,7 @@ ```ts import { CatalogApi } from '@backstage/catalog-client'; import { CatalogProcessor } from '@backstage/plugin-catalog-node'; +import { CatalogProcessorParser } from '@backstage/plugin-catalog-node'; import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-node'; @@ -27,6 +28,7 @@ export const catalogAnalysisExtensionPoint: ExtensionPoint): void; } diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index 763e934176..09a7e1d894 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -18,6 +18,7 @@ import { createExtensionPoint } from '@backstage/backend-plugin-api'; import { Entity, Validators } from '@backstage/catalog-model'; import { CatalogProcessor, + CatalogProcessorParser, EntitiesSearchFilter, EntityProvider, PlaceholderResolver, @@ -55,6 +56,12 @@ export interface CatalogModelExtensionPoint { * @param validators - The (subset of) validators to set */ setFieldValidators(validators: Partial): void; + + /** + * Sets the entity data parser which is used to read raw data from locations + * @param parser - Parser which will used to extract entities from raw data + */ + setEntityDataParser(parser: CatalogProcessorParser): void; } /** diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 62bdf3f2c9..a492a72e9f 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -93,8 +93,12 @@ export type CatalogReactComponentsNameToClassKey = { CatalogReactEntityTagPicker: CatalogReactEntityTagPickerClassKey; CatalogReactEntityOwnerPicker: CatalogReactEntityOwnerPickerClassKey; CatalogReactEntityProcessingStatusPicker: CatalogReactEntityProcessingStatusPickerClassKey; + CatalogReactEntityAutocompletePickerClassKey: CatalogReactEntityAutocompletePickerClassKey; }; +// @public (undocumented) +export type CatalogReactEntityAutocompletePickerClassKey = 'root' | 'label'; + // @public export type CatalogReactEntityDisplayNameClassKey = 'root' | 'icon'; @@ -105,10 +109,13 @@ export type CatalogReactEntityLifecyclePickerClassKey = 'input'; export type CatalogReactEntityNamespacePickerClassKey = 'input'; // @public (undocumented) -export type CatalogReactEntityOwnerPickerClassKey = 'input'; +export type CatalogReactEntityOwnerPickerClassKey = 'input' | 'root' | 'label'; // @public (undocumented) -export type CatalogReactEntityProcessingStatusPickerClassKey = 'input'; +export type CatalogReactEntityProcessingStatusPickerClassKey = + | 'input' + | 'root' + | 'label'; // @public (undocumented) export type CatalogReactEntitySearchBarClassKey = 'searchToolbar' | 'input'; diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx index 98e680aa12..1b7539b912 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Box, TextFieldProps, Typography } from '@material-ui/core'; +import { Box, TextFieldProps, Typography, makeStyles } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; import React, { useEffect, useMemo, useState } from 'react'; @@ -52,6 +52,17 @@ export type EntityAutocompletePickerProps< initialSelectedOptions?: string[]; }; +/** @public */ +export type CatalogReactEntityAutocompletePickerClassKey = 'root' | 'label'; + +const useStyles = makeStyles( + { + root: {}, + label: {}, + }, + { name: 'CatalogReactEntityAutocompletePicker' }, +); + /** @public */ export function EntityAutocompletePicker< T extends DefaultEntityFilters = DefaultEntityFilters, @@ -67,6 +78,8 @@ export function EntityAutocompletePicker< initialSelectedOptions = [], } = props; + const classes = useStyles(); + const { updateFilters, filters, @@ -127,8 +140,8 @@ export function EntityAutocompletePicker< if (availableOptions.length <= 1) return null; return ( - - + + {label} multiple diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts b/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts index 684e7cef55..87811be894 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts @@ -16,6 +16,7 @@ export { EntityAutocompletePicker } from './EntityAutocompletePicker'; export type { + CatalogReactEntityAutocompletePickerClassKey, EntityAutocompletePickerProps, AllowedEntityFilters, } from './EntityAutocompletePicker'; diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index 5b5bd39310..87de6411b2 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -44,10 +44,12 @@ import { withStyles } from '@material-ui/core/styles'; import { useEntityPresentation } from '../../apis'; /** @public */ -export type CatalogReactEntityOwnerPickerClassKey = 'input'; +export type CatalogReactEntityOwnerPickerClassKey = 'input' | 'root' | 'label'; const useStyles = makeStyles( { + root: {}, + label: {}, input: {}, fullWidth: { width: '100%' }, boxLabel: { @@ -174,8 +176,8 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => { } return ( - - + + Owner { const availableAdvancedItems = ['Is Orphan', 'Has Error']; return ( - - + + Processing Status multiple diff --git a/plugins/catalog-react/src/overridableComponents.ts b/plugins/catalog-react/src/overridableComponents.ts index 6424c95ab5..b45fd30103 100644 --- a/plugins/catalog-react/src/overridableComponents.ts +++ b/plugins/catalog-react/src/overridableComponents.ts @@ -26,6 +26,7 @@ import { CatalogReactEntityOwnerPickerClassKey, CatalogReactEntityProcessingStatusPickerClassKey, } from './components'; +import { CatalogReactEntityAutocompletePickerClassKey } from './components/EntityAutocompletePicker/EntityAutocompletePicker'; /** @public */ export type CatalogReactComponentsNameToClassKey = { @@ -36,6 +37,7 @@ export type CatalogReactComponentsNameToClassKey = { CatalogReactEntityTagPicker: CatalogReactEntityTagPickerClassKey; CatalogReactEntityOwnerPicker: CatalogReactEntityOwnerPickerClassKey; CatalogReactEntityProcessingStatusPicker: CatalogReactEntityProcessingStatusPickerClassKey; + CatalogReactEntityAutocompletePickerClassKey: CatalogReactEntityAutocompletePickerClassKey; }; /** @public */ diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index 15f7d6cc91..5c583e6a19 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -65,22 +65,23 @@ export const catalogEntityPage = createPageExtension({ loader: async ({ inputs }) => { const { EntityLayout } = await import('../components/EntityLayout'); const Component = () => { - const { entity, ...rest } = useEntityFromUrl(); return ( - - {entity ? ( - - {inputs.contents - .filter(({ output: { filterFunction, filterExpression } }) => - buildFilterFn(filterFunction, filterExpression)(entity), - ) - .map(({ output: { path, title, element } }) => ( - - {element} - - ))} - - ) : null} + + + {inputs.contents.map(({ output }) => ( + + {output.element} + + ))} + ); }; diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 4260a0c26b..a955aaadb9 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -86,6 +86,21 @@ export const columnFactories = Object.freeze({ return { title: 'Targets', field: 'entity.spec.targets', + customFilterAndSearch: (query, row) => { + let targets: JsonArray = []; + if ( + row.entity?.spec?.targets && + Array.isArray(row.entity?.spec?.targets) + ) { + targets = row.entity?.spec?.targets; + } else if (row.entity?.spec?.target) { + targets = [row.entity?.spec?.target]; + } + return targets + .join(', ') + .toLocaleUpperCase('en-US') + .includes(query.toLocaleUpperCase('en-US')); + }, render: ({ entity }) => ( <> {(entity?.spec?.targets || entity?.spec?.target) && ( diff --git a/plugins/code-coverage-backend/api-report.md b/plugins/code-coverage-backend/api-report.md index 8a64361391..fef869a302 100644 --- a/plugins/code-coverage-backend/api-report.md +++ b/plugins/code-coverage-backend/api-report.md @@ -3,10 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -21,6 +23,8 @@ export function createRouter(options: RouterOptions): Promise; // @public export interface RouterOptions { + // (undocumented) + auth?: AuthService; // (undocumented) catalogApi?: CatalogApi; // (undocumented) @@ -30,6 +34,8 @@ export interface RouterOptions { // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) + httpAuth?: HttpAuthService; + // (undocumented) logger: Logger; // (undocumented) urlReader: UrlReader; diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index b3c3aae2d7..b44f608bbb 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -43,11 +43,12 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "knex": "^3.0.0", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/body-parser-xml": "^2.0.2", "@types/supertest": "^2.0.8", diff --git a/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts b/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts index 80de585d44..b6b43234e6 100644 --- a/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts +++ b/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts @@ -54,4 +54,10 @@ describe('convert jacoco', () => { expect(files.sort()).toEqual(expected.sort()); }); + + it('works when not providing files (as per not setting annotation to scm-only)', () => { + const files = converter.convert(fixture, []); + + expect(files).toHaveLength(4); + }); }); diff --git a/plugins/code-coverage-backend/src/service/converter/jacoco.ts b/plugins/code-coverage-backend/src/service/converter/jacoco.ts index ac5ccbfa45..d461cfe7d7 100644 --- a/plugins/code-coverage-backend/src/service/converter/jacoco.ts +++ b/plugins/code-coverage-backend/src/service/converter/jacoco.ts @@ -40,7 +40,6 @@ export class Jacoco implements Converter { */ convert(xml: JacocoXML, scmFiles: Array): Array { const jscov: Array = []; - xml.report.package.forEach(r => { const packageName = r.$.name; r.sourcefile.forEach(sf => { @@ -68,9 +67,12 @@ export class Jacoco implements Converter { .map(f => f.trimEnd()) .find(f => f.endsWith(packageAndFilename)); this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`); - if (Object.keys(lineHits).length > 0 && currentFile) { + if ( + scmFiles.length === 0 || + (Object.keys(lineHits).length > 0 && currentFile) + ) { jscov.push({ - filename: currentFile, + filename: currentFile || packageAndFilename, branchHits: branchHits, lineHits: lineHits, }); diff --git a/plugins/code-coverage-backend/src/service/router.test.ts b/plugins/code-coverage-backend/src/service/router.test.ts index 2032a5470d..64ea515ac0 100644 --- a/plugins/code-coverage-backend/src/service/router.test.ts +++ b/plugins/code-coverage-backend/src/service/router.test.ts @@ -26,6 +26,7 @@ import { import { ConfigReader } from '@backstage/config'; import { createRouter } from './router'; import { CatalogRequestOptions } from '@backstage/catalog-client'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; jest.mock('./CodeCoverageDatabase'); @@ -96,6 +97,8 @@ describe('createRouter', () => { discovery: testDiscovery, urlReader: mockUrlReader, logger: getVoidLogger(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = express().use(router); }); @@ -118,21 +121,21 @@ describe('createRouter', () => { '/history?entity=component:default/mycomponent', ].forEach(uri => { describe(`GET ${uri}`, () => { - it('does not send token when calling catalog api and request is unauthenticated', async () => { - const response = await request(app).get(uri); - - expect(response.status).toEqual(200); - expect(catalogRequestOptions.token).toBeUndefined(); - }); - - it('includes auth token when calling catalog api', async () => { - const token = 'my-auth-token'; + it('forwards request credentials to the catalog api call', async () => { const response = await request(app) .get(uri) - .set('Authorization', `Bearer ${token}`); + .set( + 'Authorization', + mockCredentials.user.header('user:default/other'), + ); expect(response.status).toEqual(200); - expect(catalogRequestOptions.token).toEqual(token); + expect(catalogRequestOptions.token).toEqual( + mockCredentials.service.token({ + onBehalfOf: mockCredentials.user('user:default/other'), + targetPluginId: 'catalog', + }), + ); }); }); }); diff --git a/plugins/code-coverage-backend/src/service/router.ts b/plugins/code-coverage-backend/src/service/router.ts index 47bc285d62..d1a0a1703c 100644 --- a/plugins/code-coverage-backend/src/service/router.ts +++ b/plugins/code-coverage-backend/src/service/router.ts @@ -21,6 +21,7 @@ import BodyParser from 'body-parser'; import bodyParserXml from 'body-parser-xml'; import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; import { + createLegacyAuthAdapters, errorHandler, PluginDatabaseManager, PluginEndpointDiscovery, @@ -33,7 +34,7 @@ import { CodeCoverageDatabase } from './CodeCoverageDatabase'; import { aggregateCoverage, CoverageUtils } from './CoverageUtils'; import { Converter, Jacoco, Cobertura, Lcov } from './converter'; import { getEntitySourceLocation } from '@backstage/catalog-model'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; /** * Options for {@link createRouter}. @@ -47,6 +48,8 @@ export interface RouterOptions { urlReader: UrlReader; logger: Logger; catalogApi?: CatalogApi; + auth?: AuthService; + httpAuth?: HttpAuthService; } export interface CodeCoverageApi { @@ -63,6 +66,7 @@ export const makeRouter = async ( const catalogApi = options.catalogApi ?? new CatalogClient({ discoveryApi: discovery }); const scm = ScmIntegrations.fromConfig(config); + const { auth, httpAuth } = createLegacyAuthAdapters(options); const bodySizeLimit = config.getOptionalString('codeCoverage.bodySizeLimit') ?? '100kb'; @@ -92,9 +96,13 @@ export const makeRouter = async ( */ router.get('/report', async (req, res) => { const { entity } = req.query; - const entityLookup = await catalogApi.getEntityByRef(entity as string, { - token: getBearerTokenFromAuthorizationHeader(req.headers.authorization), - }); + const entityLookup = await catalogApi.getEntityByRef( + entity as string, + await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }), + ); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); } @@ -116,9 +124,13 @@ export const makeRouter = async ( */ router.get('/history', async (req, res) => { const { entity } = req.query; - const entityLookup = await catalogApi.getEntityByRef(entity as string, { - token: getBearerTokenFromAuthorizationHeader(req.headers.authorization), - }); + const entityLookup = await catalogApi.getEntityByRef( + entity as string, + await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }), + ); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); } @@ -136,9 +148,13 @@ export const makeRouter = async ( */ router.get('/file-content', async (req, res) => { const { entity, path } = req.query; - const entityLookup = await catalogApi.getEntityByRef(entity as string, { - token: getBearerTokenFromAuthorizationHeader(req.headers.authorization), - }); + const entityLookup = await catalogApi.getEntityByRef( + entity as string, + await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }), + ); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); } @@ -189,9 +205,13 @@ export const makeRouter = async ( */ router.post('/report', async (req, res) => { const { entity: entityRef, coverageType } = req.query; - const entity = await catalogApi.getEntityByRef(entityRef as string, { - token: getBearerTokenFromAuthorizationHeader(req.headers.authorization), - }); + const entity = await catalogApi.getEntityByRef( + entityRef as string, + await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }), + ); if (!entity) { throw new NotFoundError(`No entity found matching ${entityRef}`); } diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 4bc0454ab0..50c6119655 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -58,7 +58,7 @@ "react-use": "^17.2.4", "recharts": "^2.5.0", "regression": "^2.0.1", - "yup": "^0.32.9" + "yup": "^1.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/events-backend-module-aws-sqs/README.md b/plugins/events-backend-module-aws-sqs/README.md index 7ca4bb280d..bd8e0c51d3 100644 --- a/plugins/events-backend-module-aws-sqs/README.md +++ b/plugins/events-backend-module-aws-sqs/README.md @@ -1,12 +1,12 @@ -# events-backend-module-aws-sqs +# `@backstage/plugins-events-backend-module-aws-sqs` -Welcome to the `events-backend-module-aws-sqs` backend plugin! +Welcome to the `events-backend-module-aws-sqs` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `AwsSqsConsumingEventPublisher`. +This package is a module for the `events-backend` backend plugin +and extends the events system with an `AwsSqsConsumingEventPublisher`. -This event publisher will allow you to receive events from -an AWS SQS queue and will publish these to the used event broker. +This event publisher will allow you to receive events from an AWS SQS queue +and will publish these to the used `EventsService` implementation. ## Configuration @@ -32,15 +32,43 @@ events: ## Installation -1. Install the [`events-backend` plugin](../events-backend/README.md). -2. Install this module -3. Add your configuration. +1. Install this module +2. Add your configuration. ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-aws-sqs ``` -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha')); ``` + +### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +// ... +import { AwsSqsConsumingEventPublisher } from '@backstage/plugin-events-backend-module-aws-sqs'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const eventsRouter = Router(); + + // ... + + const sqs = AwsSqsConsumingEventPublisher.fromConfig({ + config: env.config, + events: env.events, + logger: env.logger, + scheduler: env.scheduler, + }); + await Promise.all(sqs.map(publisher => publisher.start())); + + return eventsRouter; +} +``` diff --git a/plugins/events-backend-module-aws-sqs/api-report.md b/plugins/events-backend-module-aws-sqs/api-report.md index ffa9c888d6..ff863e4d8d 100644 --- a/plugins/events-backend-module-aws-sqs/api-report.md +++ b/plugins/events-backend-module-aws-sqs/api-report.md @@ -4,20 +4,20 @@ ```ts import { Config } from '@backstage/config'; -import { EventBroker } from '@backstage/plugin-events-node'; -import { EventPublisher } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; +import { EventsService } from '@backstage/plugin-events-node'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; // @public -export class AwsSqsConsumingEventPublisher implements EventPublisher { +export class AwsSqsConsumingEventPublisher { // (undocumented) static fromConfig(env: { config: Config; - logger: Logger; + events: EventsService; + logger: LoggerService; scheduler: PluginTaskScheduler; }): AwsSqsConsumingEventPublisher[]; // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; + start(): Promise; } ``` diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 0b70eb57bf..41f91d804e 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -48,8 +48,7 @@ "@backstage/config": "workspace:^", "@backstage/plugin-events-node": "workspace:^", "@backstage/types": "workspace:^", - "luxon": "^3.0.0", - "winston": "^3.2.1" + "luxon": "^3.0.0" }, "devDependencies": { "@aws-sdk/types": "^3.347.0", diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts index e32245b483..60930fdcf7 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts @@ -22,7 +22,7 @@ import { import { getVoidLogger } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { ConfigReader } from '@backstage/config'; -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { mockClient } from 'aws-sdk-client-mock'; import { AwsSqsConsumingEventPublisher } from './AwsSqsConsumingEventPublisher'; @@ -53,12 +53,14 @@ describe('AwsSqsConsumingEventPublisher', () => { }, }); const logger = getVoidLogger(); + const events = new TestEventsService(); const scheduler = { scheduleTask: jest.fn(), } as unknown as PluginTaskScheduler; const publishers = AwsSqsConsumingEventPublisher.fromConfig({ config, + events, logger, scheduler, }); @@ -85,21 +87,21 @@ describe('AwsSqsConsumingEventPublisher', () => { }, }); const logger = getVoidLogger(); + const events = new TestEventsService(); const scheduler = { scheduleTask: jest.fn(), } as unknown as PluginTaskScheduler; const publishers = AwsSqsConsumingEventPublisher.fromConfig({ config, + events, logger, scheduler, }); expect(publishers.length).toEqual(1); const publisher = publishers[0]; - - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); + await publisher.start(); // publisher.connect(..) was causing the polling for events to be scheduled expect(scheduler.scheduleTask).toHaveBeenCalledWith( @@ -133,6 +135,7 @@ describe('AwsSqsConsumingEventPublisher', () => { }, }); const logger = getVoidLogger(); + const events = new TestEventsService(); let taskFn: (() => Promise) | undefined = undefined; const scheduler = { scheduleTask: (spec: { fn: () => Promise }) => { @@ -196,32 +199,31 @@ describe('AwsSqsConsumingEventPublisher', () => { const publishers = AwsSqsConsumingEventPublisher.fromConfig({ config, + events, logger, scheduler, }); expect(publishers.length).toEqual(1); const publisher = publishers[0]; - - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); + await publisher.start(); await taskFn!(); await taskFn!(); await taskFn!(); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('fake1'); - expect(eventBroker.published[0].eventPayload).toEqual({ + expect(events.published).toHaveLength(2); + expect(events.published[0].topic).toEqual('fake1'); + expect(events.published[0].eventPayload).toEqual({ event: 'payload1', }); - expect(eventBroker.published[0].metadata).toEqual({ + expect(events.published[0].metadata).toEqual({ 'X-Custom-Attr': 'value', }); - expect(eventBroker.published[1].topic).toEqual('fake1'); - expect(eventBroker.published[1].eventPayload).toEqual({ + expect(events.published[1].topic).toEqual('fake1'); + expect(events.published[1].eventPayload).toEqual({ event: 'payload2', }); - expect(eventBroker.published[1].metadata).toEqual({}); + expect(events.published[1].metadata).toEqual({}); }); }); diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts index a2f6a00fe7..26eba88c18 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts @@ -21,10 +21,10 @@ import { ReceiveMessageCommandInput, SQSClient, } from '@aws-sdk/client-sqs'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { Config } from '@backstage/config'; -import { EventBroker, EventPublisher } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; +import { EventsService } from '@backstage/plugin-events-node'; import { AwsSqsEventSourceConfig, readConfig } from './config'; /** @@ -34,28 +34,34 @@ import { AwsSqsEventSourceConfig, readConfig } from './config'; * @public */ // TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) -export class AwsSqsConsumingEventPublisher implements EventPublisher { +export class AwsSqsConsumingEventPublisher { private readonly topic: string; private readonly receiveParams: ReceiveMessageCommandInput; private readonly sqs: SQSClient; private readonly queueUrl: string; private readonly taskTimeoutSeconds: number; private readonly waitTimeAfterEmptyReceiveMs; - private eventBroker?: EventBroker; static fromConfig(env: { config: Config; - logger: Logger; + events: EventsService; + logger: LoggerService; scheduler: PluginTaskScheduler; }): AwsSqsConsumingEventPublisher[] { return readConfig(env.config).map( config => - new AwsSqsConsumingEventPublisher(env.logger, env.scheduler, config), + new AwsSqsConsumingEventPublisher( + env.logger, + env.events, + env.scheduler, + config, + ), ); } private constructor( - private readonly logger: Logger, + private readonly logger: LoggerService, + private readonly events: EventsService, private readonly scheduler: PluginTaskScheduler, config: AwsSqsEventSourceConfig, ) { @@ -80,12 +86,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { config.waitTimeAfterEmptyReceive.as('milliseconds'); } - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - return this.start(); - } - - private async start(): Promise { + async start(): Promise { const id = `events.awsSqs.publisher:${this.topic}`; const logger = this.logger.child({ class: AwsSqsConsumingEventPublisher.prototype.constructor.name, @@ -172,7 +173,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { } }); - this.eventBroker!.publish({ + this.events.publish({ topic: this.topic, eventPayload, metadata, diff --git a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts index 57e1831667..cbf29f96cd 100644 --- a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts +++ b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts @@ -14,26 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { eventsModuleAwsSqsConsumingEventPublisher } from './eventsModuleAwsSqsConsumingEventPublisher'; -import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher'; describe('eventsModuleAwsSqsConsumingEventPublisher', () => { it('should be correctly wired and set up', async () => { - let addedPublishers: AwsSqsConsumingEventPublisher[] | undefined; - const extensionPoint = { - addPublishers: (publishers: any) => { - addedPublishers = publishers; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - }; + }); const scheduler = mockServices.scheduler.mock(); await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], features: [ + eventsServiceFactory(), eventsModuleAwsSqsConsumingEventPublisher(), mockServices.rootConfig.factory({ data: { @@ -65,14 +67,6 @@ describe('eventsModuleAwsSqsConsumingEventPublisher', () => { ], }); - expect(addedPublishers).not.toBeUndefined(); - expect(addedPublishers!.length).toEqual(2); - - const eventBroker = new TestEventBroker(); - await Promise.all( - addedPublishers!.map(publisher => publisher.setEventBroker(eventBroker)), - ); - // publisher.connect(..) was causing the polling for events to be scheduled expect(scheduler.scheduleTask).toHaveBeenCalledWith( expect.objectContaining({ id: 'events.awsSqs.publisher:fake1' }), diff --git a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts index eabab94708..ea0f094bee 100644 --- a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts @@ -18,8 +18,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher'; /** @@ -34,19 +33,19 @@ export const eventsModuleAwsSqsConsumingEventPublisher = createBackendModule({ env.registerInit({ deps: { config: coreServices.rootConfig, - events: eventsExtensionPoint, + events: eventsServiceRef, logger: coreServices.logger, scheduler: coreServices.scheduler, }, async init({ config, events, logger, scheduler }) { - const winstonLogger = loggerToWinstonLogger(logger); const sqs = AwsSqsConsumingEventPublisher.fromConfig({ - config: config, - logger: winstonLogger, - scheduler: scheduler, + config, + events, + logger, + scheduler, }); - events.addPublishers(sqs); + await Promise.all(sqs.map(publisher => publisher.start())); }, }); }, diff --git a/plugins/events-backend-module-azure/README.md b/plugins/events-backend-module-azure/README.md index 61b3b63175..7c3920d499 100644 --- a/plugins/events-backend-module-azure/README.md +++ b/plugins/events-backend-module-azure/README.md @@ -1,9 +1,9 @@ # events-backend-module-azure -Welcome to the `events-backend-module-azure` backend plugin! +Welcome to the `events-backend-module-azure` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `AzureDevOpsEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `AzureDevOpsEventRouter`. The event router will subscribe to the topic `azureDevOps` and route the events to more concrete topics based on the value @@ -22,30 +22,22 @@ and [webhooks](https://learn.microsoft.com/en-us/azure/devops/service-hooks/serv ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-azure ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend-module-azure/alpha')); ``` -### Add to backend (old) +### Legacy Backend System -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: - -```diff -+const azureEventRouter = new AzureDevOpsEventRouter(); - -new EventsBackend(env.logger) -+ .addPublishers(azureEventRouter) -+ .addSubscribers(azureEventRouter); -// [...] +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new AzureDevOpsEventRouter({ + events: env.events, +}); +await eventRouter.subscribe(); ``` diff --git a/plugins/events-backend-module-azure/api-report.md b/plugins/events-backend-module-azure/api-report.md index 4460aeb510..66529ef2d4 100644 --- a/plugins/events-backend-module-azure/api-report.md +++ b/plugins/events-backend-module-azure/api-report.md @@ -4,12 +4,15 @@ ```ts import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; // @public export class AzureDevOpsEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index 9187f74024..0ee9f2dbfc 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -42,8 +42,7 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts index 56e761a8fc..837888a05a 100644 --- a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts +++ b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { AzureDevOpsEventRouter } from './AzureDevOpsEventRouter'; describe('AzureDevOpsEventRouter', () => { - const eventRouter = new AzureDevOpsEventRouter(); + const events = new TestEventsService(); + const eventRouter = new AzureDevOpsEventRouter({ events: events }); const topic = 'azureDevOps'; const eventPayload = { eventType: 'test.type', test: 'payload' }; const metadata = {}; - it('no $.eventType', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('AzureDevOpsEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.eventType', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with $.eventType', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('azureDevOps.test.type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published).toHaveLength(1); + expect(events.published[0].topic).toEqual('azureDevOps.test.type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts index 11dd7546dd..de05abd032 100644 --- a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts +++ b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class AzureDevOpsEventRouter extends SubTopicEventRouter { - constructor() { - super('azureDevOps'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'azureDevOps', + }); + } + + protected getSubscriberId(): string { + return 'AzureDevOpsEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts index d67b5eb071..d2afb81851 100644 --- a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts +++ b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleAzureDevOpsEventRouter } from './eventsModuleAzureDevOpsEventRouter'; -import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; describe('eventsModuleAzureDevOpsEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: AzureDevOpsEventRouter | undefined; - let addedSubscriber: AzureDevOpsEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleAzureDevOpsEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(AzureDevOpsEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(AzureDevOpsEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleAzureDevOpsEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('AzureDevOpsEventRouter'); }); }); diff --git a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts index 50bc384502..9741015477 100644 --- a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts +++ b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; /** @@ -31,13 +31,13 @@ export const eventsModuleAzureDevOpsEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new AzureDevOpsEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new AzureDevOpsEventRouter({ + events, + }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-bitbucket-cloud/README.md b/plugins/events-backend-module-bitbucket-cloud/README.md index 0a40ab2eea..7ff743c06d 100644 --- a/plugins/events-backend-module-bitbucket-cloud/README.md +++ b/plugins/events-backend-module-bitbucket-cloud/README.md @@ -1,9 +1,9 @@ # events-backend-module-bitbucket-cloud -Welcome to the `events-backend-module-bitbucket-cloud` backend plugin! +Welcome to the `events-backend-module-bitbucket-cloud` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `BitbucketCloudEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `BitbucketCloudEventRouter`. The event router will subscribe to the topic `bitbucketCloud` and route the events to more concrete topics based on the value @@ -22,32 +22,24 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-bitbucket-cloud ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add( import('@backstage/plugin-events-backend-module-bitbucket-cloud/alpha'), ); ``` -### Add to backend (old) +### Legacy Backend System -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: - -```diff -+const bitbucketCloudEventRouter = new BitbucketCloudEventRouter(); - -new EventsBackend(env.logger) -+ .addPublishers(bitbucketCloudEventRouter) -+ .addSubscribers(bitbucketCloudEventRouter); -// [...] +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new BitbucketCloudEventRouter({ + events: env.events, +}); +await eventRouter.subscribe(); ``` diff --git a/plugins/events-backend-module-bitbucket-cloud/api-report.md b/plugins/events-backend-module-bitbucket-cloud/api-report.md index 4795edd89a..ba4f61d739 100644 --- a/plugins/events-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/events-backend-module-bitbucket-cloud/api-report.md @@ -4,12 +4,15 @@ ```ts import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; // @public export class BitbucketCloudEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 609fac199c..5273652f44 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -42,8 +42,7 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts index b7a47984e4..65a4f1bf21 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts @@ -14,33 +14,40 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { BitbucketCloudEventRouter } from './BitbucketCloudEventRouter'; describe('BitbucketCloudEventRouter', () => { - const eventRouter = new BitbucketCloudEventRouter(); + const events = new TestEventsService(); + const eventRouter = new BitbucketCloudEventRouter({ events }); const topic = 'bitbucketCloud'; const eventPayload = { test: 'payload' }; const metadata = { 'x-event-key': 'test:type' }; - it('no x-event-key', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('BitbucketCloudEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no x-event-key', () => { eventRouter.onEvent({ topic, eventPayload }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with x-event-key', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('bitbucketCloud.test:type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('bitbucketCloud.test:type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts index 8350511d65..0f3ce09abf 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class BitbucketCloudEventRouter extends SubTopicEventRouter { - constructor() { - super('bitbucketCloud'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'bitbucketCloud', + }); + } + + protected getSubscriberId(): string { + return 'BitbucketCloudEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts index 025d994b4b..337e2206e4 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts @@ -14,32 +14,31 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleBitbucketCloudEventRouter } from './eventsModuleBitbucketCloudEventRouter'; -import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; describe('eventsModuleBitbucketCloudEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: BitbucketCloudEventRouter | undefined; - let addedSubscriber: BitbucketCloudEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleBitbucketCloudEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(BitbucketCloudEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(BitbucketCloudEventRouter); + await startTestBackend({ + features: [ + eventsServiceFactory(), + eventsModuleBitbucketCloudEventRouter(), + ], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('BitbucketCloudEventRouter'); }); }); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts index 841a463001..648d6c67fb 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; /** @@ -31,13 +31,13 @@ export const eventsModuleBitbucketCloudEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new BitbucketCloudEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new BitbucketCloudEventRouter({ + events, + }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-gerrit/README.md b/plugins/events-backend-module-gerrit/README.md index b658fba366..d5b9f9683a 100644 --- a/plugins/events-backend-module-gerrit/README.md +++ b/plugins/events-backend-module-gerrit/README.md @@ -1,8 +1,8 @@ # events-backend-module-gerrit -Welcome to the `events-backend-module-gerrit` backend plugin! +Welcome to the `events-backend-module-gerrit` backend module! -This plugin is a module for the `events-backend` backend plugin +This package is a module for the `events-backend` backend plugin and extends it with an `GerritEventRouter`. The event router will subscribe to the topic `gerrit` @@ -21,30 +21,20 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-gerrit ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend-module-gerrit/alpha')); ``` -### Add to backend (old) +### Legacy Backend System -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: - -```diff -+const gerritEventRouter = new GerritEventRouter(); - -new EventsBackend(env.logger) -+ .addPublishers(gerritEventRouter) -+ .addSubscribers(gerritEventRouter); -// [...] +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new GerritEventRouter({ events: env.events }); +await eventRouter.subscribe(); ``` diff --git a/plugins/events-backend-module-gerrit/api-report.md b/plugins/events-backend-module-gerrit/api-report.md index ba3c4dd29f..c75857aa43 100644 --- a/plugins/events-backend-module-gerrit/api-report.md +++ b/plugins/events-backend-module-gerrit/api-report.md @@ -4,12 +4,15 @@ ```ts import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; // @public export class GerritEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 889c60c685..436fbde891 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -42,8 +42,7 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts index 7302a26012..6c635fcbdf 100644 --- a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts +++ b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { GerritEventRouter } from './GerritEventRouter'; describe('GerritEventRouter', () => { - const eventRouter = new GerritEventRouter(); + const events = new TestEventsService(); + const eventRouter = new GerritEventRouter({ events: events }); const topic = 'gerrit'; const eventPayload = { type: 'test-type', test: 'payload' }; const metadata = {}; - it('no $.type', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GerritEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.type', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with $.type', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('gerrit.test-type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('gerrit.test-type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts index 3d97508b62..dac5aa34d5 100644 --- a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts +++ b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class GerritEventRouter extends SubTopicEventRouter { - constructor() { - super('gerrit'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'gerrit', + }); + } + + protected getSubscriberId(): string { + return 'GerritEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts index c11f4c42db..4fc971fcce 100644 --- a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts +++ b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { eventsModuleGerritEventRouter } from './eventsModuleGerritEventRouter'; -import { GerritEventRouter } from '../router/GerritEventRouter'; describe('eventsModuleGerritEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GerritEventRouter | undefined; - let addedSubscriber: GerritEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGerritEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GerritEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GerritEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleGerritEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GerritEventRouter'); }); }); diff --git a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts index 780ff7f878..8d9792c4f4 100644 --- a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts +++ b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { GerritEventRouter } from '../router/GerritEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGerritEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new GerritEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new GerritEventRouter({ events }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-github/README.md b/plugins/events-backend-module-github/README.md index 072877ee86..fec27b62e3 100644 --- a/plugins/events-backend-module-github/README.md +++ b/plugins/events-backend-module-github/README.md @@ -1,9 +1,9 @@ # events-backend-module-github -Welcome to the `events-backend-module-github` backend plugin! +Welcome to the `events-backend-module-github` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `GithubEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `GithubEventRouter`. The event router will subscribe to the topic `github` and route the events to more concrete topics based on the value @@ -22,37 +22,49 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-github ``` -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: +### Event Router -```diff -+const githubEventRouter = new GithubEventRouter(); +```ts +// packages/backend/src/index.ts +import { eventsModuleGithubEventRouter } from '@backstage/plugin-events-backend-module-github/alpha'; +// ... +backend.add(eventsModuleGithubEventRouter()); +``` -new EventsBackend(env.logger) -+ .addPublishers(githubEventRouter) -+ .addSubscribers(githubEventRouter); -// [...] +#### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new GithubEventRouter({ events: env.events }); +await eventRouter.subscribe(); ``` ### Signature Validator +```ts +// packages/backend/src/index.ts +import { eventsModuleGithubWebhook } from '@backstage/plugin-events-backend-module-github/alpha'; +// ... +backend.add(eventsModuleGithubWebhook()); +``` + +#### Legacy Backend System + Add the signature validator for the topic `github`: ```diff -// at packages/backend/src/plugins/events.ts +// packages/backend/src/plugins/events.ts + import { createGithubSignatureValidator } from '@backstage/plugin-events-backend-module-github'; -// [...] - const http = HttpPostIngressEventPublisher.fromConfig({ - config: env.config, - ingresses: { + // [...] + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + ingresses: { + github: { + validator: createGithubSignatureValidator(env.config), + }, @@ -61,7 +73,7 @@ Add the signature validator for the topic `github`: }); ``` -Additionally, you need to add the configuration: +## Configuration ```yaml events: diff --git a/plugins/events-backend-module-github/api-report.md b/plugins/events-backend-module-github/api-report.md index bcf9b8ed74..5341f86039 100644 --- a/plugins/events-backend-module-github/api-report.md +++ b/plugins/events-backend-module-github/api-report.md @@ -5,6 +5,7 @@ ```ts import { Config } from '@backstage/config'; import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { RequestValidator } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -15,8 +16,10 @@ export function createGithubSignatureValidator( // @public export class GithubEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index f78858d981..209167f64c 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -44,8 +44,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-events-node": "workspace:^", - "@octokit/webhooks-methods": "^3.0.0", - "winston": "^3.2.1" + "@octokit/webhooks-methods": "^3.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts b/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts index 14cf6b9933..47f7eeb99f 100644 --- a/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts +++ b/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts @@ -14,33 +14,40 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { GithubEventRouter } from './GithubEventRouter'; describe('GithubEventRouter', () => { - const eventRouter = new GithubEventRouter(); + const events = new TestEventsService(); + const eventRouter = new GithubEventRouter({ events: events }); const topic = 'github'; const eventPayload = { test: 'payload' }; const metadata = { 'x-github-event': 'test_type' }; - it('no x-github-event', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GithubEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no x-github-event', () => { eventRouter.onEvent({ topic, eventPayload }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with x-github-event', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('github.test_type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('github.test_type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-github/src/router/GithubEventRouter.ts b/plugins/events-backend-module-github/src/router/GithubEventRouter.ts index 10dd1c55c6..767ed784f2 100644 --- a/plugins/events-backend-module-github/src/router/GithubEventRouter.ts +++ b/plugins/events-backend-module-github/src/router/GithubEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class GithubEventRouter extends SubTopicEventRouter { - constructor() { - super('github'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'github', + }); + } + + protected getSubscriberId(): string { + return 'GithubEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts index 02151d0dcf..f147bbcb69 100644 --- a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts +++ b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleGithubEventRouter } from './eventsModuleGithubEventRouter'; -import { GithubEventRouter } from '../router/GithubEventRouter'; describe('eventsModuleGithubEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GithubEventRouter | undefined; - let addedSubscriber: GithubEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGithubEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GithubEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GithubEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleGithubEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GithubEventRouter'); }); }); diff --git a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts index 093307dfaf..694b4b162d 100644 --- a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts +++ b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { GithubEventRouter } from '../router/GithubEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGithubEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new GithubEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new GithubEventRouter({ events }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-gitlab/README.md b/plugins/events-backend-module-gitlab/README.md index 3d4919302f..73d5bf2d87 100644 --- a/plugins/events-backend-module-gitlab/README.md +++ b/plugins/events-backend-module-gitlab/README.md @@ -1,9 +1,9 @@ # events-backend-module-gitlab -Welcome to the `events-backend-module-gitlab` backend plugin! +Welcome to the `events-backend-module-gitlab` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `GitlabEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `GitlabEventRouter`. The event router will subscribe to the topic `gitlab` and route the events to more concrete topics based on the value @@ -21,37 +21,49 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-gitlab ``` -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: +### Event Router -```diff -+const gitlabEventRouter = new GitlabEventRouter(); +```ts +// packages/backend/src/index.ts +import { eventsModuleGitlabEventRouter } from '@backstage/plugin-events-backend-module-gitlab/alpha'; +// ... +backend.add(eventsModuleGitlabEventRouter()); +``` -new EventsBackend(env.logger) -+ .addPublishers(gitlabEventRouter) -+ .addSubscribers(gitlabEventRouter); -// [...] +#### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new GitlabEventRouter({ events: env.events }); +await eventRouter.subscribe(); ``` ### Token Validator +```ts +// packages/backend/src/index.ts +import { eventsModuleGitlabWebhook } from '@backstage/plugin-events-backend-module-gitlab/alpha'; +// ... +backend.add(eventsModuleGitlabWebhook()); +``` + +#### Legacy Backend System + Add the token validator for the topic `gitlab`: ```diff -// at packages/backend/src/plugins/events.ts +// packages/backend/src/plugins/events.ts + import { createGitlabTokenValidator } from '@backstage/plugin-events-backend-module-gitlab'; -// [...] - const http = HttpPostIngressEventPublisher.fromConfig({ - config: env.config, - ingresses: { + // [...] + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + ingresses: { + gitlab: { + validator: createGitlabTokenValidator(env.config), + }, @@ -60,7 +72,7 @@ Add the token validator for the topic `gitlab`: }); ``` -Additionally, you need to add the configuration: +## Configuration ```yaml events: diff --git a/plugins/events-backend-module-gitlab/api-report.md b/plugins/events-backend-module-gitlab/api-report.md index 8a0c513857..f348436375 100644 --- a/plugins/events-backend-module-gitlab/api-report.md +++ b/plugins/events-backend-module-gitlab/api-report.md @@ -5,6 +5,7 @@ ```ts import { Config } from '@backstage/config'; import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { RequestValidator } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -13,8 +14,10 @@ export function createGitlabTokenValidator(config: Config): RequestValidator; // @public export class GitlabEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index f59b6e5faf..08abb12b8f 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -43,8 +43,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts index 6ced12d3cb..bc9da24cbe 100644 --- a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts +++ b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { GitlabEventRouter } from './GitlabEventRouter'; describe('GitlabEventRouter', () => { - const eventRouter = new GitlabEventRouter(); + const events = new TestEventsService(); + const eventRouter = new GitlabEventRouter({ events: events }); const topic = 'gitlab'; const eventPayload = { event_name: 'test_type', test: 'payload' }; const metadata = {}; - it('no $.event_name', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GitlabEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.event_name', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with $.event_name', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('gitlab.test_type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('gitlab.test_type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts index 16324340ee..23b0389b55 100644 --- a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts +++ b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class GitlabEventRouter extends SubTopicEventRouter { - constructor() { - super('gitlab'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'gitlab', + }); + } + + protected getSubscriberId(): string { + return 'GitlabEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts index 34a68ccbe4..9195be7a73 100644 --- a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts +++ b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleGitlabEventRouter } from './eventsModuleGitlabEventRouter'; -import { GitlabEventRouter } from '../router/GitlabEventRouter'; describe('eventsModuleGitlabEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GitlabEventRouter | undefined; - let addedSubscriber: GitlabEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGitlabEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GitlabEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GitlabEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleGitlabEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GitlabEventRouter'); }); }); diff --git a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts index fc44e95057..66245efb58 100644 --- a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts +++ b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { GitlabEventRouter } from '../router/GitlabEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGitlabEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new GitlabEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new GitlabEventRouter({ events: events }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-test-utils/README.md b/plugins/events-backend-test-utils/README.md index c8727b536b..84a1be754e 100644 --- a/plugins/events-backend-test-utils/README.md +++ b/plugins/events-backend-test-utils/README.md @@ -1,4 +1,5 @@ -# plugin-events-backend-test-utils +# `@backstage/plugin-events-backend-test-utils` -Houses implementations of plugin-events-node interfaces -which can be useful for test for events-backend and its modules. +This is a package that can be used as `devDependency` +and provides a test implementation for the `EventsService` +by [`events-node` package](../events-node/README.md): `TestEventsService`. diff --git a/plugins/events-backend-test-utils/api-report.md b/plugins/events-backend-test-utils/api-report.md index 46131c4244..9630c3d4e2 100644 --- a/plugins/events-backend-test-utils/api-report.md +++ b/plugins/events-backend-test-utils/api-report.md @@ -6,9 +6,11 @@ import { EventBroker } from '@backstage/plugin-events-node'; import { EventParams } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; +import { EventsServiceSubscribeOptions } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; -// @public (undocumented) +// @public @deprecated (undocumented) export class TestEventBroker implements EventBroker { // (undocumented) publish(params: EventParams): Promise; @@ -22,7 +24,7 @@ export class TestEventBroker implements EventBroker { readonly subscribed: EventSubscriber[]; } -// @public (undocumented) +// @public @deprecated (undocumented) export class TestEventPublisher implements EventPublisher { // (undocumented) get eventBroker(): EventBroker | undefined; @@ -31,6 +33,20 @@ export class TestEventPublisher implements EventPublisher { } // @public (undocumented) +export class TestEventsService implements EventsService { + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + get published(): EventParams[]; + // (undocumented) + reset(): void; + // (undocumented) + subscribe(options: EventsServiceSubscribeOptions): Promise; + // (undocumented) + get subscribed(): EventsServiceSubscribeOptions[]; +} + +// @public @deprecated (undocumented) export class TestEventSubscriber implements EventSubscriber { constructor(name: string, topics: string[]); // (undocumented) diff --git a/plugins/events-backend-test-utils/src/deprecated.ts b/plugins/events-backend-test-utils/src/deprecated.ts new file mode 100644 index 0000000000..15072dcfb4 --- /dev/null +++ b/plugins/events-backend-test-utils/src/deprecated.ts @@ -0,0 +1,19 @@ +/* + * 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 { TestEventBroker } from './testUtils/TestEventBroker'; +export { TestEventPublisher } from './testUtils/TestEventPublisher'; +export { TestEventSubscriber } from './testUtils/TestEventSubscriber'; diff --git a/plugins/events-backend-test-utils/src/index.ts b/plugins/events-backend-test-utils/src/index.ts index efba5be0d0..da090488fc 100644 --- a/plugins/events-backend-test-utils/src/index.ts +++ b/plugins/events-backend-test-utils/src/index.ts @@ -20,4 +20,5 @@ * @packageDocumentation */ +export * from './deprecated'; export * from './testUtils'; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts index c697a6506f..78556cfc63 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts @@ -20,7 +20,10 @@ import { EventSubscriber, } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated use `TestEventsService` instead + */ export class TestEventBroker implements EventBroker { readonly published: EventParams[] = []; readonly subscribed: EventSubscriber[] = []; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts index c1b2038afb..51bad11278 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts @@ -16,7 +16,10 @@ import { EventBroker, EventPublisher } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated `EventPublisher` was replaced by `EventsService.publish` + */ export class TestEventPublisher implements EventPublisher { #eventBroker?: EventBroker; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts index ef5758b804..3db9023a9b 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts @@ -16,7 +16,10 @@ import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated `EventSubscriber` was replaced by `EventsService.subscribe`. + */ export class TestEventSubscriber implements EventSubscriber { readonly name: string; readonly topics: string[]; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts new file mode 100644 index 0000000000..c87072711c --- /dev/null +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts @@ -0,0 +1,48 @@ +/* + * 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 { + EventParams, + EventsService, + EventsServiceSubscribeOptions, +} from '@backstage/plugin-events-node'; + +/** @public */ +export class TestEventsService implements EventsService { + #published: EventParams[] = []; + #subscribed: EventsServiceSubscribeOptions[] = []; + + async publish(params: EventParams): Promise { + this.#published.push(params); + } + + async subscribe(options: EventsServiceSubscribeOptions): Promise { + this.#subscribed.push(options); + } + + get published(): EventParams[] { + return this.#published; + } + + get subscribed(): EventsServiceSubscribeOptions[] { + return this.#subscribed; + } + + reset(): void { + this.#published = []; + this.#subscribed = []; + } +} diff --git a/plugins/events-backend-test-utils/src/testUtils/index.ts b/plugins/events-backend-test-utils/src/testUtils/index.ts index a571ba3075..d9eb544628 100644 --- a/plugins/events-backend-test-utils/src/testUtils/index.ts +++ b/plugins/events-backend-test-utils/src/testUtils/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -export { TestEventBroker } from './TestEventBroker'; -export { TestEventPublisher } from './TestEventPublisher'; -export { TestEventSubscriber } from './TestEventSubscriber'; +export { TestEventsService } from './TestEventsService'; diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index b8470101aa..2dba259986 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -1,166 +1,55 @@ -# events-backend +# `@backstage/plugin-events-backend` Welcome to the events-backend backend plugin! -This plugin provides the wiring of all extension points -for managing events as defined by [plugin-events-node](../events-node) -including backend plugin `EventsPlugin` and `EventsBackend`. - -Additionally, it uses a simple in-process implementation for -the `EventBroker` by default which you can replace with a more sophisticated -implementation of your choice as you need (e.g., via module). - -Some of these (non-exhaustive) may provide added persistence, -or use external systems like AWS EventBridge, AWS SNS, Kafka, etc. +This package is based on [events-node](../events-node) and its `eventsServiceRef` +that is at the core of the event support. +It provides an `eventsPlugin` (exported as `default`). By default, the plugin ships with support to receive events via HTTP endpoints -`POST /api/events/http/{topic}` and will publish these -to the used event broker. +`POST /api/events/http/{topic}` and will publish these to the `EventsService`. + +HTTP ingresses can be enabled by config, or using the extension point +of the `eventsPlugin`. +Additionally, the latter allows to add a request validator +(e.g., signature verification). ## Installation ```bash # From your Backstage root directory -yarn --cwd packages/backend add @backstage/plugin-events-backend @backstage/plugin-events-node +yarn --cwd packages/backend add @backstage/plugin-events-backend ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend/alpha')); ``` -### Add to backend (old) +### Legacy Backend System -#### Event Broker +```ts +// packages/backend/src/plugins/events.ts +import { HttpPostIngressEventPublisher } from '@backstage/plugin-events-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; -First you will need to add and implementation of the `EventBroker` interface to the backend plugin environment. -This will allow event broker instance any backend plugins to publish and subscribe to events in order to communicate -between them. - -Add the following to `makeCreateEnv` - -```diff -// packages/backend/src/index.ts -+ import { DefaultEventBroker } from '@backstage/plugin-events-backend'; -+ const eventBroker = new DefaultEventBroker(root.child({ type: 'plugin' })); -``` - -Then update plugin environment to include the event broker. - -```diff -// packages/backend/src/types.ts -+ import { EventBroker } from '@backstage/plugin-events-node'; -+ eventBroker: EventBroker; -``` - -#### Publishing and Subscribing to events with the broker - -Backend plugins are passed the event broker in the plugin environment at startup of the application. The plugin can -make use of this to communicate between parts of the application. - -Here is an example of a plugin publishing a payload to a topic. - -```typescript jsx export default async function createPlugin( env: PluginEnvironment, ): Promise { - env.eventBroker.publish({ - topic: 'publish.example', - eventPayload: { message: 'Hello, World!' }, - metadata: {}, + const eventsRouter = Router(); + + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + logger: env.logger, }); + http.bind(eventsRouter); + + return eventsRouter; } ``` -Here is an example of a plugin subscribing to a topic. - -```typescript jsx -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - env.eventBroker.subscribe([ - { - supportsEventTopics: ['publish.example'], - onEvent: async (params: EventParams) => { - env.logger.info(`receieved ${params.topic} event`); - }, - }, - ]); -} -``` - -#### Implementing an `EventSubscriber` class - -More complex solutions might need the creation of a class that implements the `EventSubscriber` interface. e.g. - -```typescript jsx -import { EventSubscriber } from './EventSubscriber'; - -class ExampleSubscriber implements EventSubscriber { - // ... - - supportsEventTopics() { - return ['publish.example']; - } - - async onEvent(params: EventParams) { - env.logger.info(`receieved ${params.topic} event`); - } -} -``` - -#### Events Backend - -The events backend plugin provides a router to handler http events and publish the http requests onto the event -broker. - -To configure it add a file [`packages/backend/src/plugins/events.ts`](../../packages/backend/src/plugins/events.ts) -to your Backstage project. - -Additionally, add the events plugin to your backend. - -```diff -// packages/backend/src/index.ts -// [...] -+import events from './plugins/events'; -// [...] -+ const eventsEnv = useHotMemoize(module, () => createEnv('events')); -// [...] -+ apiRouter.use('/events', await events(eventsEnv)); -// [...] -``` - -#### Configuration - -In order to create HTTP endpoints to receive events for a certain -topic, you need to add them at your configuration: - -```yaml -events: - http: - topics: - - bitbucketCloud - - github - - whatever -``` - -Only those topics added to the configuration will result in -available endpoints. - -The example above would result in the following endpoints: - -``` -POST /api/events/http/bitbucketCloud -POST /api/events/http/github -POST /api/events/http/whatever -``` - -You may want to use these for webhooks by SCM providers -in combination with suitable event subscribers. - -However, it is not limited to these use cases. - ### Event-based Entity Providers You can implement the `EventSubscriber` interface on an `EntityProviders` to allow it to handle events from other plugins e.g. the event backend plugin @@ -189,74 +78,42 @@ Assuming you have configured the `eventBroker` into the `PluginEnvironment` you } ``` +## Configuration + +In order to create HTTP endpoints to receive events for a certain +topic, you need to add them at your configuration: + +```yaml +events: + http: + topics: + - bitbucketCloud + - github + - whatever +``` + +Only those topics added to the configuration will result in +available endpoints. + +The example above would result in the following endpoints: + +``` +POST /api/events/http/bitbucketCloud +POST /api/events/http/github +POST /api/events/http/whatever +``` + +You may want to use these for webhooks by SCM providers +in combination with suitable event subscribers. + +However, it is not limited to these use cases. + ## Use Cases -### Custom Event Broker - -Example using the `EventsBackend`: - -```ts -new EventsBackend(env.logger) - .setEventBroker(yourEventBroker) - // [...] - .start(); -``` - -Example using a module: - -```ts -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; - -// [...] - -export const yourModuleEventsModule = createBackendModule({ - pluginId: 'events', - moduleId: 'your-module', - register(env) { - // [...] - env.registerInit({ - deps: { - // [...] - events: eventsExtensionPoint, - // [...] - }, - async init({ /* ... */ events /*, ... */ }) { - // [...] - const yourEventBroker = new YourEventBroker(); - // [...] - events.setEventBroker(yourEventBroker); - }, - }); - }, -}); -``` - ### Request Validator -Example using the `EventsBackend`: - ```ts -const http = HttpPostIngressEventPublisher.fromConfig({ - config: env.config, - ingresses: { - yourTopic: { - validator: yourValidator, - }, - }, - logger: env.logger, -}); -http.bind(router); - -await new EventsBackend(env.logger) - .addPublishers(http) - // [...] - .start(); -``` - -Example using a module: - -```ts -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; // [...] @@ -282,3 +139,19 @@ export const eventsModuleYourFeature = createBackendModule({ }, }); ``` + +#### Legacy Backend System + +```ts +const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + ingresses: { + yourTopic: { + validator: yourValidator, + }, + }, + logger: env.logger, +}); +http.bind(router); +``` diff --git a/plugins/events-backend/api-report.md b/plugins/events-backend/api-report.md index aeb6f9363d..8b5d4e8d31 100644 --- a/plugins/events-backend/api-report.md +++ b/plugins/events-backend/api-report.md @@ -7,14 +7,17 @@ import { Config } from '@backstage/config'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventParams } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; import express from 'express'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; -// @public +// @public @deprecated export class DefaultEventBroker implements EventBroker { - constructor(logger: Logger); + // @deprecated + constructor(logger: LoggerService, events?: EventsService); // (undocumented) publish(params: EventParams): Promise; // (undocumented) @@ -23,7 +26,7 @@ export class DefaultEventBroker implements EventBroker { ): void; } -// @public +// @public @deprecated export class EventsBackend { constructor(logger: Logger); // (undocumented) @@ -40,18 +43,17 @@ export class EventsBackend { } // @public -export class HttpPostIngressEventPublisher implements EventPublisher { +export class HttpPostIngressEventPublisher { // (undocumented) bind(router: express.Router): void; // (undocumented) static fromConfig(env: { config: Config; + events: EventsService; ingresses?: { [topic: string]: Omit; }; - logger: Logger; + logger: LoggerService; }): HttpPostIngressEventPublisher; - // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; } ``` diff --git a/plugins/events-backend/src/deprecated.ts b/plugins/events-backend/src/deprecated.ts new file mode 100644 index 0000000000..cce853b2af --- /dev/null +++ b/plugins/events-backend/src/deprecated.ts @@ -0,0 +1,18 @@ +/* + * 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 { EventsBackend } from './service/EventsBackend'; +export { DefaultEventBroker } from './service/DefaultEventBroker'; diff --git a/plugins/events-backend/src/index.ts b/plugins/events-backend/src/index.ts index be173b677c..63dfa5d252 100644 --- a/plugins/events-backend/src/index.ts +++ b/plugins/events-backend/src/index.ts @@ -20,6 +20,5 @@ * @packageDocumentation */ -export { EventsBackend } from './service/EventsBackend'; +export * from './deprecated'; export { HttpPostIngressEventPublisher } from './service/http'; -export { DefaultEventBroker } from './service/DefaultEventBroker'; diff --git a/plugins/events-backend/src/service/DefaultEventBroker.test.ts b/plugins/events-backend/src/service/DefaultEventBroker.test.ts index 99e5f6f72d..5317251726 100644 --- a/plugins/events-backend/src/service/DefaultEventBroker.test.ts +++ b/plugins/events-backend/src/service/DefaultEventBroker.test.ts @@ -85,15 +85,15 @@ describe('DefaultEventBroker', () => { } })(); - const errorSpy = jest.spyOn(logger, 'error'); + const warnSpy = jest.spyOn(logger, 'warn'); const eventBroker = new DefaultEventBroker(logger); eventBroker.subscribe(subscriber1); await eventBroker.publish({ topic, eventPayload: '1' }); - expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy).toHaveBeenCalledWith( - 'Subscriber "Subscriber1" failed to process event', + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "Subscriber1" failed to process event for topic "testTopic"', new Error('NOPE 1'), ); @@ -101,13 +101,13 @@ describe('DefaultEventBroker', () => { await eventBroker.publish({ topic, eventPayload: '2' }); // With two subscribers we should not halt on the first error but call all subscribers - expect(errorSpy).toHaveBeenCalledTimes(3); - expect(errorSpy).toHaveBeenCalledWith( - 'Subscriber "Subscriber1" failed to process event', + expect(warnSpy).toHaveBeenCalledTimes(3); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "Subscriber1" failed to process event for topic "testTopic"', new Error('NOPE 2'), ); - expect(errorSpy).toHaveBeenCalledWith( - 'Subscriber "Subscriber2" failed to process event', + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "Subscriber2" failed to process event for topic "testTopic"', new Error('NOPE 2'), ); }); diff --git a/plugins/events-backend/src/service/DefaultEventBroker.ts b/plugins/events-backend/src/service/DefaultEventBroker.ts index c3824b3e7d..27523f3118 100644 --- a/plugins/events-backend/src/service/DefaultEventBroker.ts +++ b/plugins/events-backend/src/service/DefaultEventBroker.ts @@ -14,12 +14,14 @@ * limitations under the License. */ +import { LoggerService } from '@backstage/backend-plugin-api'; import { + DefaultEventsService, EventBroker, EventParams, + EventsService, EventSubscriber, } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; /** * In process event broker which will pass the event to all registered subscribers @@ -27,44 +29,34 @@ import { Logger } from 'winston'; * Events will not be persisted in any form. * * @public + * @deprecated use `DefaultEventsService` from `@backstage/plugin-events-node` instead */ -// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) export class DefaultEventBroker implements EventBroker { - constructor(private readonly logger: Logger) {} + private readonly events: EventsService; - private readonly subscribers: { - [topic: string]: EventSubscriber[]; - } = {}; + /** + * + * @param logger - logger + * @param events - replacement that gets wrapped to support not yet migrated implementations. + * An instance can be passed (required for a mixed mode), otherwise a new instance gets created internally. + * @deprecated use `DefaultEventsService` directly instead + */ + constructor(logger: LoggerService, events?: EventsService) { + this.events = events ?? DefaultEventsService.create({ logger }); + } async publish(params: EventParams): Promise { - this.logger.debug( - `Event received: topic=${params.topic}, metadata=${JSON.stringify( - params.metadata, - )}, payload=${JSON.stringify(params.eventPayload)}`, - ); - - const subscribed = this.subscribers[params.topic] ?? []; - await Promise.all( - subscribed.map(async subscriber => { - try { - await subscriber.onEvent(params); - } catch (error) { - this.logger.error( - `Subscriber "${subscriber.constructor.name}" failed to process event`, - error, - ); - } - }), - ); + return this.events.publish(params); } subscribe( ...subscribers: Array> ): void { - subscribers.flat().forEach(subscriber => { - subscriber.supportsEventTopics().forEach(topic => { - this.subscribers[topic] = this.subscribers[topic] ?? []; - this.subscribers[topic].push(subscriber); + subscribers.flat().forEach(async subscriber => { + await this.events.subscribe({ + id: subscriber.constructor.name, + topics: subscriber.supportsEventTopics(), + onEvent: subscriber.onEvent.bind(subscriber), }); }); } diff --git a/plugins/events-backend/src/service/EventsBackend.ts b/plugins/events-backend/src/service/EventsBackend.ts index 4415b8703a..2c93663b46 100644 --- a/plugins/events-backend/src/service/EventsBackend.ts +++ b/plugins/events-backend/src/service/EventsBackend.ts @@ -26,6 +26,7 @@ import { DefaultEventBroker } from './DefaultEventBroker'; * A builder that helps wire up all component parts of the event management. * * @public + * @deprecated `EventBroker`, `EventPublisher`, and `EventSubscriber` got replaced by `EventsService` and its methods. */ export class EventsBackend { private eventBroker: EventBroker; diff --git a/plugins/events-backend/src/service/EventsPlugin.test.ts b/plugins/events-backend/src/service/EventsPlugin.test.ts index 7f555a0b6f..e39951e8dc 100644 --- a/plugins/events-backend/src/service/EventsPlugin.test.ts +++ b/plugins/events-backend/src/service/EventsPlugin.test.ts @@ -14,22 +14,27 @@ * limitations under the License. */ -import { createBackendModule } from '@backstage/backend-plugin-api'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { - TestEventBroker, - TestEventPublisher, - TestEventSubscriber, -} from '@backstage/plugin-events-backend-test-utils'; + createBackendModule, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import request from 'supertest'; import { eventsPlugin } from './EventsPlugin'; -describe('eventPlugin', () => { +describe('eventsPlugin', () => { it('should be initialized properly', async () => { - const eventBroker = new TestEventBroker(); - const publisher = new TestEventPublisher(); - const subscriber = new TestEventSubscriber('sub', ['fake']); + const eventsService = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return eventsService; + }, + }); const testModule = createBackendModule({ pluginId: 'events', @@ -40,9 +45,9 @@ describe('eventPlugin', () => { events: eventsExtensionPoint, }, async init({ events }) { - events.setEventBroker(eventBroker); - events.addPublishers(publisher); - events.addSubscribers(subscriber); + events.addHttpPostIngress({ + topic: 'fake-ext', + }); }, }); }, @@ -51,6 +56,7 @@ describe('eventPlugin', () => { const { server } = await startTestBackend({ extensionPoints: [], features: [ + eventsServiceFactory(), eventsPlugin(), testModule(), mockServices.logger.factory(), @@ -66,18 +72,24 @@ describe('eventPlugin', () => { ], }); - expect(publisher.eventBroker).toBe(eventBroker); - expect(eventBroker.subscribed.length).toEqual(1); - expect(eventBroker.subscribed[0]).toBe(subscriber); - - const response = await request(server) + const response1 = await request(server) .post('/api/events/http/fake') .timeout(1000) .send({ test: 'fake' }); - expect(response.status).toBe(202); + expect(response1.status).toBe(202); - expect(eventBroker.published.length).toEqual(1); - expect(eventBroker.published[0].topic).toEqual('fake'); - expect(eventBroker.published[0].eventPayload).toEqual({ test: 'fake' }); + const response2 = await request(server) + .post('/api/events/http/fake-ext') + .timeout(1000) + .send({ test: 'fake-ext' }); + expect(response2.status).toBe(202); + + expect(eventsService.published).toHaveLength(2); + expect(eventsService.published[0].topic).toEqual('fake'); + expect(eventsService.published[0].eventPayload).toEqual({ test: 'fake' }); + expect(eventsService.published[1].topic).toEqual('fake-ext'); + expect(eventsService.published[1].eventPayload).toEqual({ + test: 'fake-ext', + }); }); }); diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index 27456b5b2a..5e1df975c5 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -18,59 +18,42 @@ import { createBackendPlugin, coreServices, } from '@backstage/backend-plugin-api'; -import { loggerToWinstonLogger } from '@backstage/backend-common'; import { eventsExtensionPoint, EventsExtensionPoint, } from '@backstage/plugin-events-node/alpha'; import { - EventBroker, - EventPublisher, - EventSubscriber, + eventsServiceRef, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; -import { DefaultEventBroker } from './DefaultEventBroker'; import Router from 'express-promise-router'; import { HttpPostIngressEventPublisher } from './http'; class EventsExtensionPointImpl implements EventsExtensionPoint { - #eventBroker: EventBroker | undefined; #httpPostIngresses: HttpPostIngressOptions[] = []; - #publishers: EventPublisher[] = []; - #subscribers: EventSubscriber[] = []; - setEventBroker(eventBroker: EventBroker): void { - this.#eventBroker = eventBroker; + setEventBroker(_: any): void { + throw new Error( + 'setEventBroker is not supported anymore; use eventsServiceRef instead', + ); } - addPublishers( - ...publishers: Array> - ): void { - this.#publishers.push(...publishers.flat()); + addPublishers(_: any): void { + throw new Error( + 'addPublishers is not supported anymore; use EventsService instead', + ); } - addSubscribers( - ...subscribers: Array> - ): void { - this.#subscribers.push(...subscribers.flat()); + addSubscribers(_: any): void { + throw new Error( + 'addSubscribers is not supported anymore; use EventsService instead', + ); } addHttpPostIngress(options: HttpPostIngressOptions) { this.#httpPostIngresses.push(options); } - get eventBroker() { - return this.#eventBroker; - } - - get publishers() { - return this.#publishers; - } - - get subscribers() { - return this.#subscribers; - } - get httpPostIngresses() { return this.#httpPostIngresses; } @@ -90,12 +73,11 @@ export const eventsPlugin = createBackendPlugin({ env.registerInit({ deps: { config: coreServices.rootConfig, + events: eventsServiceRef, logger: coreServices.logger, router: coreServices.httpRouter, }, - async init({ config, logger, router }) { - const winstonLogger = loggerToWinstonLogger(logger); - + async init({ config, events, logger, router }) { const ingresses = Object.fromEntries( extensionPoint.httpPostIngresses.map(ingress => [ ingress.topic, @@ -105,20 +87,13 @@ export const eventsPlugin = createBackendPlugin({ const http = HttpPostIngressEventPublisher.fromConfig({ config, + events, ingresses, - logger: winstonLogger, + logger, }); const eventsRouter = Router(); http.bind(eventsRouter); router.use(eventsRouter); - - const eventBroker = - extensionPoint.eventBroker ?? new DefaultEventBroker(winstonLogger); - - eventBroker.subscribe(extensionPoint.subscribers); - [extensionPoint.publishers, http] - .flat() - .forEach(publisher => publisher.setEventBroker(eventBroker)); }, }); }, diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts index 72e0b94c57..665ac0b4a9 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts @@ -16,7 +16,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import express from 'express'; import Router from 'express-promise-router'; import request from 'supertest'; @@ -36,9 +36,11 @@ describe('HttpPostIngressEventPublisher', () => { const router = Router(); const app = express().use(router); + const events = new TestEventsService(); const publisher = HttpPostIngressEventPublisher.fromConfig({ config, + events, ingresses: { testB: {}, }, @@ -46,9 +48,6 @@ describe('HttpPostIngressEventPublisher', () => { }); publisher.bind(router); - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); - const notFoundResponse = await request(app) .post('/http/unknown') .timeout(1000) @@ -69,18 +68,18 @@ describe('HttpPostIngressEventPublisher', () => { .send({ testB: 'data' }); expect(response2.status).toBe(202); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('testA'); - expect(eventBroker.published[0].eventPayload).toEqual({ testA: 'data' }); - expect(eventBroker.published[0].metadata).toEqual( + expect(events.published).toHaveLength(2); + expect(events.published[0].topic).toEqual('testA'); + expect(events.published[0].eventPayload).toEqual({ testA: 'data' }); + expect(events.published[0].metadata).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-custom-header': 'test-value', }), ); - expect(eventBroker.published[1].topic).toEqual('testB'); - expect(eventBroker.published[1].eventPayload).toEqual({ testB: 'data' }); - expect(eventBroker.published[1].metadata).toEqual( + expect(events.published[1].topic).toEqual('testB'); + expect(events.published[1].eventPayload).toEqual({ testB: 'data' }); + expect(events.published[1].metadata).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-custom-header': 'test-value', @@ -99,9 +98,11 @@ describe('HttpPostIngressEventPublisher', () => { const router = Router(); const app = express().use(router); + const events = new TestEventsService(); const publisher = HttpPostIngressEventPublisher.fromConfig({ config, + events, ingresses: { testB: { validator: async (req, context) => { @@ -146,9 +147,6 @@ describe('HttpPostIngressEventPublisher', () => { }); publisher.bind(router); - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); - const response1 = await request(app) .post('/http/testA') .timeout(1000) @@ -191,12 +189,12 @@ describe('HttpPostIngressEventPublisher', () => { expect(response6.status).toBe(403); expect(response6.body).toEqual({}); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('testA'); - expect(eventBroker.published[0].eventPayload).toEqual({ test: 'data' }); - expect(eventBroker.published[1].topic).toEqual('testB'); - expect(eventBroker.published[1].eventPayload).toEqual({ test: 'data' }); - expect(eventBroker.published[1].metadata).toEqual( + expect(events.published).toHaveLength(2); + expect(events.published[0].topic).toEqual('testA'); + expect(events.published[0].eventPayload).toEqual({ test: 'data' }); + expect(events.published[1].topic).toEqual('testB'); + expect(events.published[1].eventPayload).toEqual({ test: 'data' }); + expect(events.published[1].metadata).toEqual( expect.objectContaining({ 'x-test-signature': 'testB-signature', }), @@ -205,10 +203,12 @@ describe('HttpPostIngressEventPublisher', () => { it('without configuration', async () => { const config = new ConfigReader({}); + const events = new TestEventsService(); expect(() => HttpPostIngressEventPublisher.fromConfig({ config, + events, logger, }), ).not.toThrow(); diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts index b5a6ccbca1..06dc4e463a 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts @@ -15,16 +15,15 @@ */ import { errorHandler } from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { - EventBroker, - EventPublisher, + EventsService, HttpPostIngressOptions, RequestValidator, } from '@backstage/plugin-events-node'; import express from 'express'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; import { RequestValidationContextImpl } from './validation'; /** @@ -34,13 +33,12 @@ import { RequestValidationContextImpl } from './validation'; * @public */ // TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) -export class HttpPostIngressEventPublisher implements EventPublisher { - private eventBroker?: EventBroker; - +export class HttpPostIngressEventPublisher { static fromConfig(env: { config: Config; + events: EventsService; ingresses?: { [topic: string]: Omit }; - logger: Logger; + logger: LoggerService; }): HttpPostIngressEventPublisher { const topics = env.config.getOptionalStringArray('events.http.topics') ?? []; @@ -54,11 +52,12 @@ export class HttpPostIngressEventPublisher implements EventPublisher { } }); - return new HttpPostIngressEventPublisher(env.logger, ingresses); + return new HttpPostIngressEventPublisher(env.events, env.logger, ingresses); } private constructor( - private readonly logger: Logger, + private readonly events: EventsService, + private readonly logger: LoggerService, private readonly ingresses: { [topic: string]: Omit; }, @@ -68,10 +67,6 @@ export class HttpPostIngressEventPublisher implements EventPublisher { router.use('/http', this.createRouter(this.ingresses)); } - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - } - private createRouter(ingresses: { [topic: string]: Omit; }): express.Router { @@ -108,7 +103,7 @@ export class HttpPostIngressEventPublisher implements EventPublisher { } const eventPayload = request.body; - await this.eventBroker!.publish({ + await this.events.publish({ topic, eventPayload, metadata: request.headers, diff --git a/plugins/events-node/README.md b/plugins/events-node/README.md index 44738222bc..ce4d705025 100644 --- a/plugins/events-node/README.md +++ b/plugins/events-node/README.md @@ -1,3 +1,82 @@ -# plugin-events-node +# `@backstage/plugin-events-node` -Houses types and utilities for building events-related modules. +This package defined basic types for event-based interactions inside of Backstage. + +Additionally, it provides the core event service `eventsServiceRef` of type `EventsService` +with its default implementation that uses the `DefaultEventsService` implementation. + +`DefaultEventsService` is a simple in-memory implementation +that requires the co-deployment of producers and consumers of events. + +## Installation + +Add `@backstage/plugin-events-node` as dependency to your plugin or plugin module package +to which you want to add event support. + +Use `eventsServiceRef` as a dependency at your plugin or plugin module. + +### Legacy Backend System + +Create an `EventsService` instance and add it to the environment. + +```ts +// packages/backend/src/plugins/events.ts +import { DefaultEventsService } from '@backstage/plugin-events-node'; + +// ... + +function makeCreateEnv(config: Config) { + // ... + const eventsService = DefaultEventsService.create({ logger: root }); + // ... + return (plugin: string): PluginEnvironment => { + // ... + return { + // ... + events: eventsService, + // ... + }; + }; +} +``` + +Use the `events` from the `PluginEnvironment` as desired: + +```ts +// packages/backend/src/plugins/events.ts +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + // ... + env.events; // ... + // ... +} +``` + +## Use Case + +### Exchange service implementation + +Create your custom service factory implementation: + +```ts +import { eventsServiceRef } from '@backstage/plugin-events-node'; +// ... +export const customEventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: { + // add needed dependencies here + }, + async factory({ logger }) { + // add your custom logic here + return customEventsService; + }, +}); +``` + +and your custom implementation: + +```diff +// packages/backend/src/index.ts ++ backend.add(customEventsServiceFactory()); +``` diff --git a/plugins/events-node/api-report-alpha.md b/plugins/events-node/api-report-alpha.md index fd30f54d45..f61048ac94 100644 --- a/plugins/events-node/api-report-alpha.md +++ b/plugins/events-node/api-report-alpha.md @@ -13,15 +13,15 @@ import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; export interface EventsExtensionPoint { // (undocumented) addHttpPostIngress(options: HttpPostIngressOptions): void; - // (undocumented) + // @deprecated (undocumented) addPublishers( ...publishers: Array> ): void; - // (undocumented) + // @deprecated (undocumented) addSubscribers( ...subscribers: Array> ): void; - // (undocumented) + // @deprecated (undocumented) setEventBroker(eventBroker: EventBroker): void; } diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index dfda48d97e..9574d6c098 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -3,7 +3,21 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { LoggerService } from '@backstage/backend-plugin-api'; +import { ServiceRef } from '@backstage/backend-plugin-api'; + // @public +export class DefaultEventsService implements EventsService { + // (undocumented) + static create(options: { logger: LoggerService }): DefaultEventsService; + forPlugin(pluginId: string): EventsService; + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + subscribe(options: EventsServiceSubscribeOptions): Promise; +} + +// @public @deprecated export interface EventBroker { publish(params: EventParams): Promise; subscribe( @@ -18,29 +32,50 @@ export interface EventParams { topic: string; } -// @public +// @public @deprecated export interface EventPublisher { - // (undocumented) + // @deprecated (undocumented) setEventBroker(eventBroker: EventBroker): Promise; } // @public -export abstract class EventRouter implements EventPublisher, EventSubscriber { +export abstract class EventRouter { + protected constructor(options: { events: EventsService; topics: string[] }); // (undocumented) protected abstract determineDestinationTopic( params: EventParams, ): string | undefined; // (undocumented) + protected abstract getSubscriberId(): string; + // (undocumented) onEvent(params: EventParams): Promise; - // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; - // (undocumented) - abstract supportsEventTopics(): string[]; + subscribe(): Promise; } // @public +export interface EventsService { + publish(params: EventParams): Promise; + subscribe(options: EventsServiceSubscribeOptions): Promise; +} + +// @public (undocumented) +export type EventsServiceEventHandler = (params: EventParams) => Promise; + +// @public +export const eventsServiceRef: ServiceRef; + +// @public (undocumented) +export type EventsServiceSubscribeOptions = { + id: string; + topics: string[]; + onEvent: EventsServiceEventHandler; +}; + +// @public @deprecated export interface EventSubscriber { + // @deprecated onEvent(params: EventParams): Promise; + // @deprecated supportsEventTopics(): string[]; } @@ -79,12 +114,10 @@ export type RequestValidator = ( // @public export abstract class SubTopicEventRouter extends EventRouter { - protected constructor(topic: string); + protected constructor(options: { events: EventsService; topic: string }); // (undocumented) protected determineDestinationTopic(params: EventParams): string | undefined; // (undocumented) protected abstract determineSubTopic(params: EventParams): string | undefined; - // (undocumented) - supportsEventTopics(): string[]; } ``` diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 58ddd50413..de794dfa45 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -45,6 +45,7 @@ "@backstage/backend-plugin-api": "workspace:^" }, "devDependencies": { + "@backstage/backend-common": "workspace:^", "@backstage/cli": "workspace:^" }, "files": [ diff --git a/plugins/events-node/src/api/DefaultEventsService.test.ts b/plugins/events-node/src/api/DefaultEventsService.test.ts new file mode 100644 index 0000000000..33df923892 --- /dev/null +++ b/plugins/events-node/src/api/DefaultEventsService.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { DefaultEventsService } from './DefaultEventsService'; +import { EventParams } from './EventParams'; + +const logger = getVoidLogger(); + +describe('DefaultEventsService', () => { + it('passes events to interested subscribers', async () => { + const events = DefaultEventsService.create({ logger }); + const eventsSubscriber1: EventParams[] = []; + const eventsSubscriber2: EventParams[] = []; + + await events.subscribe({ + id: 'subscriber1', + topics: ['topicA', 'topicB'], + onEvent: async event => { + eventsSubscriber1.push(event); + }, + }); + await events.subscribe({ + id: 'subscriber2', + topics: ['topicB', 'topicC'], + onEvent: async event => { + eventsSubscriber2.push(event); + }, + }); + await events.publish({ + topic: 'topicA', + eventPayload: { test: 'topicA' }, + }); + await events.publish({ + topic: 'topicB', + eventPayload: { test: 'topicB' }, + }); + await events.publish({ + topic: 'topicC', + eventPayload: { test: 'topicC' }, + }); + await events.publish({ + topic: 'topicD', + eventPayload: { test: 'topicD' }, + }); + + expect(eventsSubscriber1).toEqual([ + { topic: 'topicA', eventPayload: { test: 'topicA' } }, + { topic: 'topicB', eventPayload: { test: 'topicB' } }, + ]); + expect(eventsSubscriber2).toEqual([ + { topic: 'topicB', eventPayload: { test: 'topicB' } }, + { topic: 'topicC', eventPayload: { test: 'topicC' } }, + ]); + }); + + it('logs errors from subscribers', async () => { + const topic = 'testTopic'; + + const warnSpy = jest.spyOn(logger, 'warn'); + const events = DefaultEventsService.create({ logger }); + + await events.subscribe({ + id: 'subscriber1', + topics: [topic], + onEvent: event => { + throw new Error(`NOPE ${event.eventPayload}`); + }, + }); + await events.publish({ topic, eventPayload: '1' }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber1" failed to process event for topic "testTopic"', + new Error('NOPE 1'), + ); + + await events.subscribe({ + id: 'subscriber2', + topics: [topic], + onEvent: event => { + throw new Error(`NOPE ${event.eventPayload}`); + }, + }); + await events.publish({ topic, eventPayload: '2' }); + + // With two subscribers we should not halt on the first error but call all subscribers + expect(warnSpy).toHaveBeenCalledTimes(3); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber1" failed to process event for topic "testTopic"', + new Error('NOPE 2'), + ); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber2" failed to process event for topic "testTopic"', + new Error('NOPE 2'), + ); + }); +}); diff --git a/plugins/events-node/src/api/DefaultEventsService.ts b/plugins/events-node/src/api/DefaultEventsService.ts new file mode 100644 index 0000000000..bb5c2a0ca8 --- /dev/null +++ b/plugins/events-node/src/api/DefaultEventsService.ts @@ -0,0 +1,104 @@ +/* + * 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 { LoggerService } from '@backstage/backend-plugin-api'; +import { EventParams } from './EventParams'; +import { EventsService, EventsServiceSubscribeOptions } from './EventsService'; + +/** + * In-process event broker which will pass the event to all registered subscribers + * interested in it. + * Events will not be persisted in any form. + * Events will not be passed to subscribers at other instances of the same cluster. + * + * @public + */ +// TODO(pjungermann): add opentelemetry? (see plugins/catalog-backend/src/util/opentelemetry.ts, etc.) +export class DefaultEventsService implements EventsService { + private readonly subscribers = new Map< + string, + Omit[] + >(); + + private constructor(private readonly logger: LoggerService) {} + + static create(options: { logger: LoggerService }): DefaultEventsService { + return new DefaultEventsService(options.logger); + } + + /** + * Returns a plugin-scoped context of the `EventService` + * that ensures to prefix subscriber IDs with the plugin ID. + * + * @param pluginId - The plugin that the `EventService` should be created for. + */ + forPlugin(pluginId: string): EventsService { + return { + publish: (params: EventParams): Promise => { + return this.publish(params); + }, + subscribe: (options: EventsServiceSubscribeOptions): Promise => { + return this.subscribe({ + ...options, + id: `${pluginId}.${options.id}`, + }); + }, + }; + } + + async publish(params: EventParams): Promise { + this.logger.debug( + `Event received: topic=${params.topic}, metadata=${JSON.stringify( + params.metadata, + )}, payload=${JSON.stringify(params.eventPayload)}`, + ); + + if (!this.subscribers.has(params.topic)) { + return; + } + + const onEventPromises: Promise[] = []; + this.subscribers.get(params.topic)?.forEach(subscription => { + onEventPromises.push( + (async () => { + try { + await subscription.onEvent(params); + } catch (error) { + this.logger.warn( + `Subscriber "${subscription.id}" failed to process event for topic "${params.topic}"`, + error, + ); + } + })(), + ); + }); + + await Promise.all(onEventPromises); + } + + async subscribe(options: EventsServiceSubscribeOptions): Promise { + options.topics.forEach(topic => { + if (!this.subscribers.has(topic)) { + this.subscribers.set(topic, []); + } + + this.subscribers.get(topic)!.push({ + id: options.id, + onEvent: options.onEvent, + }); + }); + } +} diff --git a/plugins/events-node/src/api/EventBroker.ts b/plugins/events-node/src/api/EventBroker.ts index 736c2a2bf0..f6afdf2f14 100644 --- a/plugins/events-node/src/api/EventBroker.ts +++ b/plugins/events-node/src/api/EventBroker.ts @@ -23,6 +23,7 @@ import { EventSubscriber } from './EventSubscriber'; * others can subscribe for future events for topics they are interested in. * * @public + * @deprecated use `EventsService` instead */ export interface EventBroker { /** diff --git a/plugins/events-node/src/api/EventPublisher.ts b/plugins/events-node/src/api/EventPublisher.ts index 285f427804..9089eb33fa 100644 --- a/plugins/events-node/src/api/EventPublisher.ts +++ b/plugins/events-node/src/api/EventPublisher.ts @@ -23,7 +23,11 @@ import { EventBroker } from './EventBroker'; * or from event brokers, queues, etc. * * @public + * @deprecated use the `EventsService` via the constructor, setter, or other means instead */ export interface EventPublisher { + /** + * @deprecated use the `EventsService` via the constructor, setter, or other means instead + */ setEventBroker(eventBroker: EventBroker): Promise; } diff --git a/plugins/events-node/src/api/EventRouter.test.ts b/plugins/events-node/src/api/EventRouter.test.ts index 551c5ea67d..f7709ead64 100644 --- a/plugins/events-node/src/api/EventRouter.test.ts +++ b/plugins/events-node/src/api/EventRouter.test.ts @@ -14,11 +14,19 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; import { EventRouter } from './EventRouter'; +import { EventsService } from './EventsService'; class TestEventRouter extends EventRouter { + constructor(events: EventsService) { + super({ events, topics: ['my-topic'] }); + } + + protected getSubscriberId(): string { + return 'TestEventRouter'; + } + protected determineDestinationTopic(params: EventParams): string | undefined { const payload = params.eventPayload as { value?: number }; if (payload.value === undefined) { @@ -27,26 +35,21 @@ class TestEventRouter extends EventRouter { return payload.value % 2 === 0 ? 'even' : 'odd'; } - - supportsEventTopics(): string[] { - return ['my-topic']; - } } describe('EventRouter', () => { - const eventRouter = new TestEventRouter(); + const published: EventParams[] = []; + const events: EventsService = { + publish: async event => { + published.push(event); + }, + subscribe: async _subscription => {}, + }; + const eventRouter = new TestEventRouter(events); const topic = 'my-topic'; const metadata = { random: 'metadata' }; it('no destination topic', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload: { discarded: 'event' }, @@ -57,14 +60,6 @@ describe('EventRouter', () => { }); it('with destination topic', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - const payloadEven = { value: 2 }; const payloadOdd = { value: 3 }; await eventRouter.onEvent({ topic, eventPayload: payloadEven, metadata }); diff --git a/plugins/events-node/src/api/EventRouter.ts b/plugins/events-node/src/api/EventRouter.ts index b435ef15f4..5e4492d25f 100644 --- a/plugins/events-node/src/api/EventRouter.ts +++ b/plugins/events-node/src/api/EventRouter.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; -import { EventPublisher } from './EventPublisher'; -import { EventSubscriber } from './EventSubscriber'; +import { EventsService } from './EventsService'; /** * Subscribes to a topic and - depending on a set of conditions - @@ -26,13 +24,41 @@ import { EventSubscriber } from './EventSubscriber'; * @see {@link https://www.enterpriseintegrationpatterns.com/MessageRouter.html | Message Router pattern}. * @public */ -export abstract class EventRouter implements EventPublisher, EventSubscriber { - private eventBroker?: EventBroker; +export abstract class EventRouter { + private readonly events: EventsService; + private readonly topics: string[]; + private subscribed: boolean = false; + + protected constructor(options: { events: EventsService; topics: string[] }) { + this.events = options.events; + this.topics = options.topics; + } + + protected abstract getSubscriberId(): string; protected abstract determineDestinationTopic( params: EventParams, ): string | undefined; + /** + * Subscribes itself to the topic(s), + * after which events potentially can be received + * and processed by {@link EventRouter.onEvent}. + */ + async subscribe(): Promise { + if (this.subscribed) { + return; + } + + this.subscribed = true; + + await this.events.subscribe({ + id: this.getSubscriberId(), + topics: this.topics, + onEvent: this.onEvent.bind(this), + }); + } + async onEvent(params: EventParams): Promise { const topic = this.determineDestinationTopic(params); @@ -41,15 +67,9 @@ export abstract class EventRouter implements EventPublisher, EventSubscriber { } // republish to different topic - this.eventBroker?.publish({ + await this.events.publish({ ...params, topic, }); } - - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - } - - abstract supportsEventTopics(): string[]; } diff --git a/plugins/events-node/src/api/EventSubscriber.ts b/plugins/events-node/src/api/EventSubscriber.ts index 439f49b890..3686a8db3f 100644 --- a/plugins/events-node/src/api/EventSubscriber.ts +++ b/plugins/events-node/src/api/EventSubscriber.ts @@ -22,10 +22,13 @@ import { EventParams } from './EventParams'; * or other actions to react on events. * * @public + * @deprecated use the `EventsService` via the constructor, setter, or other means instead */ export interface EventSubscriber { /** * Supported event topics like "github", "bitbucketCloud", etc. + * + * @deprecated use the `EventsService` via the constructor, setter, or other means instead */ supportsEventTopics(): string[]; @@ -33,6 +36,7 @@ export interface EventSubscriber { * React on a received event. * * @param params - parameters for the to be received event. + * @deprecated you are not required to expose this anymore when using `EventsService` */ onEvent(params: EventParams): Promise; } diff --git a/plugins/events-node/src/api/EventsService.ts b/plugins/events-node/src/api/EventsService.ts new file mode 100644 index 0000000000..7af13f9b07 --- /dev/null +++ b/plugins/events-node/src/api/EventsService.ts @@ -0,0 +1,57 @@ +/* + * 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 { EventParams } from './EventParams'; + +/** + * Allows a decoupled and asynchronous communication between components. + * Components can publish events for a given topic and + * others can subscribe for future events for topics they are interested in. + * + * @public + */ +export interface EventsService { + /** + * Publishes an event for the topic. + * + * @param params - parameters for the to be published event. + */ + publish(params: EventParams): Promise; + + /** + * Subscribes to one or more topics, registering an event handler for them. + * + * @param options - event subscription options. + */ + subscribe(options: EventsServiceSubscribeOptions): Promise; +} + +/** + * @public + */ +export type EventsServiceSubscribeOptions = { + /** + * Identifier for the subscription. E.g., used as part of log messages. + */ + id: string; + topics: string[]; + onEvent: EventsServiceEventHandler; +}; + +/** + * @public + */ +export type EventsServiceEventHandler = (params: EventParams) => Promise; diff --git a/plugins/events-node/src/api/SubTopicEventRouter.test.ts b/plugins/events-node/src/api/SubTopicEventRouter.test.ts index d5c79895a5..6298e1549d 100644 --- a/plugins/events-node/src/api/SubTopicEventRouter.test.ts +++ b/plugins/events-node/src/api/SubTopicEventRouter.test.ts @@ -14,13 +14,17 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; +import { EventsService } from './EventsService'; import { SubTopicEventRouter } from './SubTopicEventRouter'; class TestSubTopicEventRouter extends SubTopicEventRouter { - constructor() { - super('my-topic'); + constructor(events: EventsService) { + super({ events, topic: 'my-topic' }); + } + + protected getSubscriberId(): string { + return 'TestSubTopicEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { @@ -29,34 +33,25 @@ class TestSubTopicEventRouter extends SubTopicEventRouter { } describe('SubTopicEventRouter', () => { - const eventRouter = new TestSubTopicEventRouter(); + const published: EventParams[] = []; + const events: EventsService = { + publish: async event => { + published.push(event); + }, + subscribe: async _subscription => {}, + }; + const eventRouter = new TestSubTopicEventRouter(events); const topic = 'my-topic'; const eventPayload = { test: 'payload' }; const metadata = { 'x-my-event': 'test.type' }; it('no x-my-event', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload }); expect(published).toEqual([]); }); it('with x-my-event', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload, metadata }); expect(published.length).toBe(1); diff --git a/plugins/events-node/src/api/SubTopicEventRouter.ts b/plugins/events-node/src/api/SubTopicEventRouter.ts index 04abe14009..5a96ad6788 100644 --- a/plugins/events-node/src/api/SubTopicEventRouter.ts +++ b/plugins/events-node/src/api/SubTopicEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams } from './EventParams'; import { EventRouter } from './EventRouter'; +import { EventsService } from './EventsService'; /** * Subscribes to the provided (generic) topic @@ -27,8 +28,11 @@ import { EventRouter } from './EventRouter'; * @public */ export abstract class SubTopicEventRouter extends EventRouter { - protected constructor(private readonly topic: string) { - super(); + protected constructor(options: { events: EventsService; topic: string }) { + super({ + events: options.events, + topics: [options.topic], + }); } protected abstract determineSubTopic(params: EventParams): string | undefined; @@ -37,8 +41,4 @@ export abstract class SubTopicEventRouter extends EventRouter { const subTopic = this.determineSubTopic(params); return subTopic ? `${params.topic}.${subTopic}` : undefined; } - - supportsEventTopics(): string[] { - return [this.topic]; - } } diff --git a/plugins/events-node/src/api/index.ts b/plugins/events-node/src/api/index.ts index 91711c0e38..94d3014dff 100644 --- a/plugins/events-node/src/api/index.ts +++ b/plugins/events-node/src/api/index.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -export type { EventBroker } from './EventBroker'; export type { EventParams } from './EventParams'; -export type { EventPublisher } from './EventPublisher'; export { EventRouter } from './EventRouter'; -export type { EventSubscriber } from './EventSubscriber'; +export type { + EventsService, + EventsServiceSubscribeOptions, + EventsServiceEventHandler, +} from './EventsService'; +export { DefaultEventsService } from './DefaultEventsService'; export * from './http'; export { SubTopicEventRouter } from './SubTopicEventRouter'; diff --git a/plugins/events-node/src/deprecated.ts b/plugins/events-node/src/deprecated.ts new file mode 100644 index 0000000000..615e7b81ea --- /dev/null +++ b/plugins/events-node/src/deprecated.ts @@ -0,0 +1,19 @@ +/* + * 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 type { EventBroker } from './api/EventBroker'; +export type { EventPublisher } from './api/EventPublisher'; +export type { EventSubscriber } from './api/EventSubscriber'; diff --git a/plugins/events-node/src/extensions.ts b/plugins/events-node/src/extensions.ts index 2e44b381af..90add52563 100644 --- a/plugins/events-node/src/extensions.ts +++ b/plugins/events-node/src/extensions.ts @@ -26,12 +26,21 @@ import { * @alpha */ export interface EventsExtensionPoint { + /** + * @deprecated use `eventsServiceRef` and `eventsServiceFactory` instead + */ setEventBroker(eventBroker: EventBroker): void; + /** + * @deprecated use `EventsService.publish` instead + */ addPublishers( ...publishers: Array> ): void; + /** + * @deprecated use `EventsService.subscribe` instead + */ addSubscribers( ...subscribers: Array> ): void; diff --git a/plugins/events-node/src/index.ts b/plugins/events-node/src/index.ts index 2bdf456f13..2bd93a8aea 100644 --- a/plugins/events-node/src/index.ts +++ b/plugins/events-node/src/index.ts @@ -21,3 +21,5 @@ */ export * from './api'; +export * from './deprecated'; +export { eventsServiceRef } from './service'; diff --git a/plugins/events-node/src/service.ts b/plugins/events-node/src/service.ts new file mode 100644 index 0000000000..e1d3047fca --- /dev/null +++ b/plugins/events-node/src/service.ts @@ -0,0 +1,47 @@ +/* + * 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, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; +import { EventsService, DefaultEventsService } from './api'; + +/** + * The {@link EventsService} that allows to publish events, and subscribe to topics. + * Uses the `root` scope so that events can be shared across all plugins, modules, and more. + * + * @public + */ +export const eventsServiceRef = createServiceRef({ + id: 'events.service', + scope: 'plugin', + defaultFactory: async service => + createServiceFactory({ + service, + deps: { + pluginMetadata: coreServices.pluginMetadata, + rootLogger: coreServices.rootLogger, + }, + async createRootContext({ rootLogger }) { + return DefaultEventsService.create({ logger: rootLogger }); + }, + async factory({ pluginMetadata }, eventsService) { + return eventsService.forPlugin(pluginMetadata.getId()); + }, + }), +}); diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index d938d9816f..c6792746e9 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -40,14 +40,14 @@ "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "@types/uuid": "^8.0.0", + "@types/uuid": "^9.0.0", "supertest": "^6.1.6" } } diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index 28dafa70bf..e1cd04d8ec 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -3,11 +3,15 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; @@ -21,12 +25,14 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { static fromConfig(options: { config: Config; catalog: CatalogApi; + discovery: DiscoveryService; + auth?: AuthService; }): DefaultJenkinsInfoProvider; // (undocumented) getInstance(opt: { entityRef: CompoundEntityRef; jobFullName?: string; - backstageToken?: string; + credentials?: BackstageCredentials; }): Promise; // (undocumented) static readonly NEW_JENKINS_ANNOTATION = 'jenkins.io/job-full-name'; @@ -61,7 +67,7 @@ export interface JenkinsInfoProvider { getInstance(options: { entityRef: CompoundEntityRef; jobFullName?: string; - backstageToken?: string; + credentials?: BackstageCredentials; }): Promise; } @@ -86,6 +92,12 @@ export default jenkinsPlugin; // @public (undocumented) export interface RouterOptions { + // (undocumented) + auth?: AuthService; + // (undocumented) + discovery: DiscoveryService; + // (undocumented) + httpAuth?: HttpAuthService; // (undocumented) jenkinsInfoProvider: JenkinsInfoProvider; // (undocumented) diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 069e4f7ae8..eaf8218a01 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -50,6 +50,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/jenkins": "^1.0.0", "@types/supertest": "^2.0.8" diff --git a/plugins/jenkins-backend/src/plugin.ts b/plugins/jenkins-backend/src/plugin.ts index 8da26d62aa..c02fc3e189 100644 --- a/plugins/jenkins-backend/src/plugin.ts +++ b/plugins/jenkins-backend/src/plugin.ts @@ -38,12 +38,24 @@ export const jenkinsPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, config: coreServices.rootConfig, catalogClient: catalogServiceRef, + discovery: coreServices.discovery, + auth: coreServices.auth, }, - async init({ logger, permissions, httpRouter, config, catalogClient }) { + async init({ + logger, + permissions, + httpRouter, + config, + catalogClient, + discovery, + auth, + }) { const winstonLogger = loggerToWinstonLogger(logger); const jenkinsInfoProvider = DefaultJenkinsInfoProvider.fromConfig({ + auth, config, catalog: catalogClient, + discovery, }); httpRouter.use( await createRouter({ @@ -56,6 +68,8 @@ export const jenkinsPlugin = createBackendPlugin({ * Info provider to be able to get all necessary information for the APIs */ jenkinsInfoProvider, + discovery, + auth, }), ); }, diff --git a/plugins/jenkins-backend/src/run.ts b/plugins/jenkins-backend/src/run.ts index 0a3ed2b7f0..95ff18a510 100644 --- a/plugins/jenkins-backend/src/run.ts +++ b/plugins/jenkins-backend/src/run.ts @@ -17,12 +17,14 @@ import { getRootLogger } from '@backstage/backend-common'; import yn from 'yn'; import { startStandaloneServer } from './service/standaloneServer'; +import { ConfigReader } from '@backstage/config'; const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); const logger = getRootLogger(); +const config = new ConfigReader({}); -startStandaloneServer({ port, enableCors, logger }).catch(err => { +startStandaloneServer({ config, port, enableCors, logger }).catch(err => { logger.error(err); process.exit(1); }); diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index 8e80de34a3..2d4fb1d62e 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -20,6 +20,7 @@ import { JenkinsInfo } from './jenkinsInfoProvider'; import { JenkinsBuild, JenkinsProject } from '../types'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import fetch, { Response } from 'node-fetch'; +import { mockServices } from '@backstage/backend-test-utils'; jest.mock('jenkins'); jest.mock('node-fetch'); @@ -716,6 +717,8 @@ describe('JenkinsApi', () => { ); }); describe('rebuildProject', () => { + const auth = mockServices.auth(); + it('successfully rebuilds', async () => { mockFetch.mockResolvedValueOnce({ status: 200 } as Response); const status = await jenkinsApi.rebuildProject( @@ -723,6 +726,7 @@ describe('JenkinsApi', () => { jobFullName, buildNumber, resourceRef, + { credentials: await auth.getOwnServiceCredentials() }, ); expect(status).toEqual(200); }); @@ -733,6 +737,7 @@ describe('JenkinsApi', () => { jobFullName, buildNumber, resourceRef, + { credentials: await auth.getOwnServiceCredentials() }, ); expect(status).toEqual(401); }); @@ -750,6 +755,7 @@ describe('JenkinsApi', () => { jobFullName, buildNumber, resourceRef, + { credentials: await auth.getOwnServiceCredentials() }, ); expect(status).toEqual(401); }); diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 2cf432343f..9171670743 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -23,12 +23,13 @@ import type { JenkinsProject, ScmDetails, } from '../types'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; import fetch, { HeaderInit } from 'node-fetch'; +import { + BackstageCredentials, + PermissionsService, +} from '@backstage/backend-plugin-api'; export class JenkinsApiImpl { private static readonly lastBuildTreeSpec = `lastBuild[ @@ -75,7 +76,7 @@ export class JenkinsApiImpl { inQueue, builds[*]`; - constructor(private readonly permissionApi?: PermissionEvaluator) {} + constructor(private readonly permissionApi?: PermissionsService) {} /** * Get a list of projects for the given JenkinsInfo. @@ -160,12 +161,12 @@ export class JenkinsApiImpl { jobFullName: string, buildNumber: number, resourceRef: string, - options?: { token?: string }, + options: { credentials: BackstageCredentials }, ): Promise { if (this.permissionApi) { const response = await this.permissionApi.authorize( [{ permission: jenkinsExecutePermission, resourceRef }], - { token: options?.token }, + { credentials: options.credentials }, ); // permission api returns always at least one item, we need to check only one result since we do not expect any additional results const { result } = response[0]; diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts index 626e5b0a5a..6be3ec572c 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts @@ -22,6 +22,7 @@ import { JenkinsConfig, JenkinsInfo, } from './jenkinsInfoProvider'; +import { mockServices } from '@backstage/backend-test-utils'; describe('JenkinsConfig', () => { it('Reads simple config and annotation', async () => { @@ -184,6 +185,8 @@ describe('DefaultJenkinsInfoProvider', () => { return DefaultJenkinsInfoProvider.fromConfig({ config, catalog: mockCatalog, + discovery: mockServices.discovery(), + auth: mockServices.auth(), }); } @@ -191,9 +194,10 @@ describe('DefaultJenkinsInfoProvider', () => { const provider = configureProvider({ jenkins: {} }, undefined); await expect(provider.getInstance({ entityRef })).rejects.toThrow(); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); }); it('Reads simple config and annotation', async () => { @@ -218,9 +222,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toStrictEqual({ baseUrl: 'https://jenkins.example.com', crumbIssuer: undefined, @@ -257,9 +262,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -296,9 +302,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -335,9 +342,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins-other.example.com', jobFullName: 'teamA/artistLookup-build', @@ -363,9 +371,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -391,9 +400,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -424,9 +434,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins-other.example.com', jobFullName: 'teamA/artistLookup-build', diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts index c4b4ef58dd..0feb91d68e 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts @@ -14,6 +14,12 @@ * limitations under the License. */ +import { createLegacyAuthAdapters } from '@backstage/backend-common'; +import { + AuthService, + BackstageCredentials, + DiscoveryService, +} from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity, @@ -34,7 +40,7 @@ export interface JenkinsInfoProvider { */ jobFullName?: string; - backstageToken?: string; + credentials?: BackstageCredentials; }): Promise; } @@ -183,27 +189,37 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { private constructor( private readonly config: JenkinsConfig, private readonly catalog: CatalogApi, + private readonly auth: AuthService, ) {} static fromConfig(options: { config: Config; catalog: CatalogApi; + discovery: DiscoveryService; + auth?: AuthService; }): DefaultJenkinsInfoProvider { + const { auth } = createLegacyAuthAdapters(options); return new DefaultJenkinsInfoProvider( JenkinsConfig.fromConfig(options.config), options.catalog, + auth, ); } async getInstance(opt: { entityRef: CompoundEntityRef; jobFullName?: string; - backstageToken?: string; + credentials?: BackstageCredentials; }): Promise { // load entity - const entity = await this.catalog.getEntityByRef(opt.entityRef, { - token: opt.backstageToken, - }); + const entity = await this.catalog.getEntityByRef( + opt.entityRef, + opt.credentials && + (await this.auth.getPluginRequestToken({ + onBehalfOf: opt.credentials, + targetPluginId: 'catalog', + })), + ); if (!entity) { throw new Error( `Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`, diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts index 06a04d8266..bda9686efb 100644 --- a/plugins/jenkins-backend/src/service/router.ts +++ b/plugins/jenkins-backend/src/service/router.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { + createLegacyAuthAdapters, + errorHandler, +} from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; @@ -25,17 +28,24 @@ import { PermissionEvaluator, toPermissionEvaluator, } from '@backstage/plugin-permission-common'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { stringifyError } from '@backstage/errors'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; import { jenkinsPermissions } from '@backstage/plugin-jenkins-common'; +import { + AuthService, + DiscoveryService, + HttpAuthService, +} from '@backstage/backend-plugin-api'; /** @public */ export interface RouterOptions { logger: Logger; jenkinsInfoProvider: JenkinsInfoProvider; permissions?: PermissionEvaluator | PermissionAuthorizer; + discovery: DiscoveryService; + auth?: AuthService; + httpAuth?: HttpAuthService; } /** @public */ @@ -56,6 +66,8 @@ export async function createRouter( : undefined; } + const { httpAuth } = createLegacyAuthAdapters(options); + const jenkinsApi = new JenkinsApiImpl(permissionEvaluator); const router = Router(); @@ -70,9 +82,6 @@ export async function createRouter( '/v1/entity/:namespace/:kind/:name/projects', async (request, response) => { const { namespace, kind, name } = request.params; - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); const branch = request.query.branch; let branches: string[] | undefined; @@ -96,7 +105,7 @@ export async function createRouter( namespace, name, }, - backstageToken: token, + credentials: await httpAuth.credentials(request), }); try { @@ -123,9 +132,6 @@ export async function createRouter( router.get( '/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber', async (request, response) => { - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); const { namespace, kind, name, jobFullName, buildNumber } = request.params; @@ -136,7 +142,7 @@ export async function createRouter( name, }, jobFullName, - backstageToken: token, + credentials: await httpAuth.credentials(request), }); const build = await jenkinsApi.getBuild( @@ -154,9 +160,6 @@ export async function createRouter( router.get( '/v1/entity/:namespace/:kind/:name/job/:jobFullName', async (request, response) => { - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); const { namespace, kind, name, jobFullName } = request.params; const jenkinsInfo = await jenkinsInfoProvider.getInstance({ @@ -166,7 +169,7 @@ export async function createRouter( name, }, jobFullName, - backstageToken: token, + credentials: await httpAuth.credentials(request), }); const build = await jenkinsApi.getJobBuilds(jenkinsInfo, jobFullName); @@ -182,9 +185,6 @@ export async function createRouter( async (request, response) => { const { namespace, kind, name, jobFullName, buildNumber } = request.params; - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); const jenkinsInfo = await jenkinsInfoProvider.getInstance({ entityRef: { kind, @@ -192,7 +192,7 @@ export async function createRouter( name, }, jobFullName, - backstageToken: token, + credentials: await httpAuth.credentials(request), }); const resourceRef = stringifyEntityRef({ kind, namespace, name }); @@ -202,7 +202,7 @@ export async function createRouter( parseInt(buildNumber, 10), resourceRef, { - token, + credentials: await httpAuth.credentials(request), }, ); response.json({}).status(status); diff --git a/plugins/jenkins-backend/src/service/standaloneServer.ts b/plugins/jenkins-backend/src/service/standaloneServer.ts index f89f235667..ab77bb6573 100644 --- a/plugins/jenkins-backend/src/service/standaloneServer.ts +++ b/plugins/jenkins-backend/src/service/standaloneServer.ts @@ -14,17 +14,19 @@ * limitations under the License. */ -import { createServiceBuilder } from '@backstage/backend-common'; +import { HostDiscovery, createServiceBuilder } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; import { createRouter } from './router'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { JenkinsInfo } from './jenkinsInfoProvider'; +import { Config } from '@backstage/config'; export interface ServerOptions { port: number; enableCors: boolean; logger: Logger; + config: Config; } export async function startStandaloneServer( @@ -41,6 +43,7 @@ export async function startStandaloneServer( return { baseUrl: 'https://example.com/', jobFullName: 'build-foo' }; }, }, + discovery: HostDiscovery.fromConfig(options.config), }); let service = createServiceBuilder(module) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index afdb3bcf9a..60331ca044 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -5,12 +5,15 @@ ```ts import { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node'; import { AuthMetadata as AuthMetadata_2 } from '@backstage/plugin-kubernetes-node'; +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { ClusterDetails as ClusterDetails_2 } from '@backstage/plugin-kubernetes-node'; import { Config } from '@backstage/config'; import { CustomResource as CustomResource_2 } from '@backstage/plugin-kubernetes-node'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import { Duration } from 'luxon'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import * as k8sAuthTypes from '@backstage/plugin-kubernetes-node'; import { KubernetesClustersSupplier as KubernetesClustersSupplier_2 } from '@backstage/plugin-kubernetes-node'; import { KubernetesCredential as KubernetesCredential_2 } from '@backstage/plugin-kubernetes-node'; @@ -190,6 +193,8 @@ export class KubernetesBuilder { catalogApi: CatalogApi, proxy: KubernetesProxy, permissionApi: PermissionEvaluator, + authService: AuthService, + httpAuth: HttpAuthService, ): express.Router; // (undocumented) protected buildServiceLocator( @@ -272,11 +277,17 @@ export type KubernetesCredential = k8sAuthTypes.KubernetesCredential; // @public (undocumented) export interface KubernetesEnvironment { + // (undocumented) + auth?: AuthService; // (undocumented) catalogApi: CatalogApi; // (undocumented) config: Config; // (undocumented) + discovery: DiscoveryService; + // (undocumented) + httpAuth?: HttpAuthService; + // (undocumented) logger: Logger; // (undocumented) permissions: PermissionEvaluator; diff --git a/plugins/kubernetes-backend/src/plugin.ts b/plugins/kubernetes-backend/src/plugin.ts index 74a8a68e53..d3aacd5190 100644 --- a/plugins/kubernetes-backend/src/plugin.ts +++ b/plugins/kubernetes-backend/src/plugin.ts @@ -178,10 +178,22 @@ export const kubernetesPlugin = createBackendPlugin({ http: coreServices.httpRouter, logger: coreServices.logger, config: coreServices.rootConfig, + discovery: coreServices.discovery, catalogApi: catalogServiceRef, permissions: coreServices.permissions, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, }, - async init({ http, logger, config, catalogApi, permissions }) { + async init({ + http, + logger, + config, + discovery, + catalogApi, + permissions, + auth, + httpAuth, + }) { const winstonLogger = loggerToWinstonLogger(logger); // TODO: expose all of the customization & extension points of the builder here const builder: KubernetesBuilder = KubernetesBuilder.createBuilder({ @@ -189,6 +201,9 @@ export const kubernetesPlugin = createBackendPlugin({ config, catalogApi, permissions, + discovery, + auth, + httpAuth, }) .setObjectsProvider(extPointObjectsProvider.getObjectsProvider()) .setClusterSupplier(extPointClusterSuplier.getClusterSupplier()) diff --git a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts index d3dcc83bb0..cfa89a6bfd 100644 --- a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts +++ b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts @@ -15,7 +15,11 @@ */ import request from 'supertest'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { + mockCredentials, + mockServices, + startTestBackend, +} from '@backstage/backend-test-utils'; import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { kubernetesObjectsProviderExtensionPoint } from '@backstage/plugin-kubernetes-node'; import { createBackendModule } from '@backstage/backend-plugin-api'; @@ -102,12 +106,6 @@ describe('resourcesRoutes', () => { }, ], }, - backend: { - auth: { - // TODO: Remove once migrated to support new auth services - dangerouslyDisableDefaultAuthPolicy: true, - }, - }, }, }), import('@backstage/plugin-kubernetes-backend/alpha'), @@ -141,7 +139,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(200, { items: [ { @@ -168,7 +165,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', message: 'entity is a required field' }, request: { @@ -189,7 +185,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -214,7 +209,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -231,6 +225,7 @@ describe('resourcesRoutes', () => { it('401 when no Auth header', async () => { await request(app) .post('/api/kubernetes/resources/workloads/query') + .set('authorization', mockCredentials.none.header()) .send({ entityRef: 'component:someComponent', auth: { @@ -239,7 +234,10 @@ describe('resourcesRoutes', () => { }) .set('Content-Type', 'application/json') .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: 'Missing credentials', + }, request: { method: 'POST', url: '/api/kubernetes/resources/workloads/query', @@ -258,9 +256,12 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'ffffff') + .set('Authorization', mockCredentials.user.invalidHeader()) .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: 'User token is invalid', + }, request: { method: 'POST', url: '/api/kubernetes/resources/workloads/query', @@ -279,7 +280,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(500, { error: { name: 'Error', @@ -312,7 +312,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(200, { items: [ { @@ -340,7 +339,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -365,7 +363,6 @@ describe('resourcesRoutes', () => { customResources: 'somestring', }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -390,7 +387,6 @@ describe('resourcesRoutes', () => { customResources: [], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -420,7 +416,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', message: 'entity is a required field' }, request: { @@ -448,7 +443,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -480,7 +474,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -497,6 +490,7 @@ describe('resourcesRoutes', () => { it('401 when no Auth header', async () => { await request(app) .post('/api/kubernetes/resources/custom/query') + .set('authorization', mockCredentials.none.header()) .send({ entityRef: 'component:someComponent', auth: { @@ -512,7 +506,10 @@ describe('resourcesRoutes', () => { }) .set('Content-Type', 'application/json') .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: 'Missing credentials', + }, request: { method: 'POST', url: '/api/kubernetes/resources/custom/query', @@ -538,9 +535,12 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'ffffff') + .set('Authorization', mockCredentials.user.invalidHeader()) .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: 'User token is invalid', + }, request: { method: 'POST', url: '/api/kubernetes/resources/custom/query', @@ -566,7 +566,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(500, { error: { name: 'Error', diff --git a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts index 0468799908..6e05f69d04 100644 --- a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts +++ b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts @@ -19,15 +19,17 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { CatalogApi } from '@backstage/catalog-client'; -import { InputError, AuthenticationError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; import express, { Request } from 'express'; import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; export const addResourceRoutesToRouter = ( router: express.Router, catalogApi: CatalogApi, objectsProvider: KubernetesObjectsProvider, + auth: AuthService, + httpAuth: HttpAuthService, ) => { const getEntityByReq = async (req: Request) => { const rawEntityRef = req.body.entityRef; @@ -44,23 +46,18 @@ export const addResourceRoutesToRouter = ( throw new InputError(`Invalid entity ref, ${error}`); } - const token = getBearerTokenFromAuthorizationHeader( - req.headers.authorization, - ); - - if (!token) { - throw new AuthenticationError('No Backstage token'); - } - - const entity = await catalogApi.getEntityByRef(entityRef, { - token: token, + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', }); + const entity = await catalogApi.getEntityByRef(entityRef, { token }); if (!entity) { throw new InputError( `Entity ref missing, ${stringifyEntityRef(entityRef)}`, ); } + return entity; }; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 295d11570f..1ac653bb58 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -36,6 +36,7 @@ import { import { setupServer } from 'msw/node'; import { ServiceMock, + mockCredentials, mockServices, setupRequestMockHandlers, startTestBackend, @@ -757,9 +758,9 @@ metadata: }); it('serves permission integration endpoint', async () => { - const response = await request(app).get( - '/api/kubernetes/.well-known/backstage/permissions/metadata', - ); + const response = await request(app) + .get('/api/kubernetes/.well-known/backstage/permissions/metadata') + .set('authorization', mockCredentials.service.header()); expect(response.status).toEqual(200); expect(response.body).toMatchObject({ diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index fec4bc5cc0..c63d039ce9 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -64,6 +64,12 @@ import { } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { KubernetesProxy } from './KubernetesProxy'; +import { createLegacyAuthAdapters } from '@backstage/backend-common'; +import { + AuthService, + DiscoveryService, + HttpAuthService, +} from '@backstage/backend-plugin-api'; /** * @@ -73,7 +79,10 @@ export interface KubernetesEnvironment { logger: Logger; config: Config; catalogApi: CatalogApi; + discovery: DiscoveryService; permissions: PermissionEvaluator; + auth?: AuthService; + httpAuth?: HttpAuthService; } /** @@ -131,6 +140,13 @@ export class KubernetesBuilder { router: Router(), } as unknown as KubernetesBuilderReturn; } + + const { auth, httpAuth } = createLegacyAuthAdapters({ + auth: this.env.auth, + httpAuth: this.env.httpAuth, + discovery: this.env.discovery, + }); + const customResources = this.buildCustomResources(); const fetcher = this.getFetcher(); @@ -158,6 +174,8 @@ export class KubernetesBuilder { this.env.catalogApi, proxy, permissions, + auth, + httpAuth, ); return { @@ -337,6 +355,8 @@ export class KubernetesBuilder { catalogApi: CatalogApi, proxy: KubernetesProxy, permissionApi: PermissionEvaluator, + authService: AuthService, + httpAuth: HttpAuthService, ): express.Router { const logger = this.env.logger; const router = Router(); @@ -391,7 +411,13 @@ export class KubernetesBuilder { }); }); - addResourceRoutesToRouter(router, catalogApi, objectsProvider); + addResourceRoutesToRouter( + router, + catalogApi, + objectsProvider, + authService, + httpAuth, + ); return router; } diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json index 0983b72f47..8b1d0104c1 100644 --- a/plugins/linguist-backend/package.json +++ b/plugins/linguist-backend/package.json @@ -52,7 +52,7 @@ "linguist-js": "^2.5.3", "luxon": "^3.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/linguist/api-report-alpha.md b/plugins/linguist/api-report-alpha.md index 418439d331..07d2244e30 100644 --- a/plugins/linguist/api-report-alpha.md +++ b/plugins/linguist/api-report-alpha.md @@ -4,10 +4,20 @@ ```ts import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) const _default: BackstagePlugin<{}, {}>; export default _default; +// @alpha (undocumented) +export const linguistTranslationRef: TranslationRef< + 'linguist', + { + readonly 'entityCard.title': 'Languages'; + readonly 'entityCard.noData': 'There is currently no language data for this entity.'; + } +>; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/linguist/src/alpha.ts b/plugins/linguist/src/alpha.ts index e80f131817..287775ade0 100644 --- a/plugins/linguist/src/alpha.ts +++ b/plugins/linguist/src/alpha.ts @@ -16,3 +16,4 @@ export * from './alpha/index'; export { default } from './alpha/index'; +export * from './translation'; diff --git a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx index 6a07a958c2..213ca316ca 100644 --- a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx +++ b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx @@ -27,6 +27,8 @@ import React from 'react'; import slugify from 'slugify'; import { useEntity } from '@backstage/plugin-catalog-react'; import { useLanguages } from '../../hooks'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { linguistTranslationRef } from '../../translation'; const useStyles = makeStyles(theme => ({ infoCard: { @@ -56,6 +58,7 @@ const useStyles = makeStyles(theme => ({ })); export const LinguistCard = () => { + const { t } = useTranslationRef(linguistTranslationRef); const classes = useStyles(); const theme = useTheme(); const { entity } = useEntity(); @@ -70,12 +73,10 @@ export const LinguistCard = () => { if (items && items.languageCount === 0 && items.totalBytes === 0) { return ( - + - - There is currently no language data for this entity. - + {t('entityCard.noData')} @@ -88,7 +89,7 @@ export const LinguistCard = () => { const processedDate = items?.processedDate; return breakdown && processedDate ? ( - + {breakdown.map((language, index: number) => { barWidth = barWidth + language.percentage; diff --git a/plugins/linguist/src/translation.ts b/plugins/linguist/src/translation.ts new file mode 100644 index 0000000000..7055d3dfe8 --- /dev/null +++ b/plugins/linguist/src/translation.ts @@ -0,0 +1,27 @@ +/* + * 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 { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const linguistTranslationRef = createTranslationRef({ + id: 'linguist', + messages: { + entityCard: { + title: 'Languages', + noData: 'There is currently no language data for this entity.', + }, + }, +}); diff --git a/plugins/notifications-backend/migrations/20240221_removeDone.js b/plugins/notifications-backend/migrations/20240221_removeDone.js new file mode 100644 index 0000000000..d67ac81825 --- /dev/null +++ b/plugins/notifications-backend/migrations/20240221_removeDone.js @@ -0,0 +1,29 @@ +/* + * 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. + */ + +exports.up = async function up(knex) { + await knex.schema.alterTable('notification', table => { + table.text('link').nullable().alter(); + table.dropColumn('done'); + }); +}; + +exports.down = async function down(knex) { + await knex.schema.alterTable('notification', table => { + table.text('link').notNullable().alter(); + table.datetime('done').nullable(); + }); +}; diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 809cab9973..8c11253a01 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -45,7 +45,7 @@ "express-promise-router": "^4.1.0", "knex": "^3.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index 35964ff582..e0af93a1d6 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -62,7 +62,6 @@ describe.each(databases.eachSupportedId())( const insertNotification = async ( notification: Partial & { id: string; - done?: Date; saved?: Date; read?: Date; }, @@ -78,7 +77,6 @@ describe.each(databases.eachSupportedId())( title: notification.payload?.title, severity: notification.payload?.severity, scope: notification.payload?.scope, - done: notification.done, saved: notification.saved, read: notification.read, }) @@ -104,44 +102,68 @@ describe.each(databases.eachSupportedId())( const notifications = await storage.getNotifications({ user }); expect(notifications.length).toBe(2); + expect(notifications.find(el => el.id === id1)).toBeTruthy(); + expect(notifications.find(el => el.id === id2)).toBeTruthy(); }); - it('should return undone notifications for user', async () => { + it('should return read notifications for user', async () => { const id1 = uuid(); const id2 = uuid(); - await insertNotification({ - id: id1, - ...testNotification, - done: new Date(), - }); + const id3 = uuid(); + await insertNotification({ id: id1, ...testNotification }); await insertNotification({ id: id2, ...testNotification }); + await insertNotification({ id: id3, ...testNotification }); await insertNotification({ id: uuid(), ...otherUserNotification }); + await storage.markRead({ ids: [id1, id3], user }); + const notifications = await storage.getNotifications({ user, - type: 'undone', + read: true, + }); + expect(notifications.length).toBe(2); + expect(notifications.find(el => el.id === id1)).toBeTruthy(); + expect(notifications.find(el => el.id === id3)).toBeTruthy(); + }); + + it('should return unread notifications for user', async () => { + const id1 = uuid(); + const id2 = uuid(); + const id3 = uuid(); + await insertNotification({ id: id1, ...testNotification }); + await insertNotification({ id: id2, ...testNotification }); + await insertNotification({ id: id3, ...testNotification }); + await insertNotification({ id: uuid(), ...otherUserNotification }); + + await storage.markRead({ ids: [id1, id3], user }); + + const notifications = await storage.getNotifications({ + user, + read: false, }); expect(notifications.length).toBe(1); expect(notifications.at(0)?.id).toEqual(id2); }); - it('should return done notifications for user', async () => { + it('should return both read and unread notifications for user', async () => { const id1 = uuid(); const id2 = uuid(); - await insertNotification({ - id: id1, - ...testNotification, - done: new Date(), - }); + const id3 = uuid(); + await insertNotification({ id: id1, ...testNotification }); await insertNotification({ id: id2, ...testNotification }); + await insertNotification({ id: id3, ...testNotification }); await insertNotification({ id: uuid(), ...otherUserNotification }); + await storage.markRead({ ids: [id1, id3], user }); + const notifications = await storage.getNotifications({ user, - type: 'done', + read: undefined, }); - expect(notifications.length).toBe(1); - expect(notifications.at(0)?.id).toEqual(id1); + expect(notifications.length).toBe(3); + expect(notifications.find(el => el.id === id1)).toBeTruthy(); + expect(notifications.find(el => el.id === id2)).toBeTruthy(); + expect(notifications.find(el => el.id === id3)).toBeTruthy(); }); it('should allow searching for notifications', async () => { @@ -166,6 +188,33 @@ describe.each(databases.eachSupportedId())( expect(notifications.length).toBe(1); expect(notifications.at(0)?.id).toEqual(id1); }); + + it('should filter notifications based on created date', async () => { + const id1 = uuid(); + const id2 = uuid(); + await insertNotification({ + id: id1, + ...testNotification, + created: new Date(Date.now() - 1 * 60 * 60 * 1000 /* an hour ago */), + }); + await insertNotification({ + id: id2, + ...testNotification, + payload: { + severity: 'normal', + title: 'Please find me', + }, + created: new Date() /* now */, + }); + await insertNotification({ id: uuid(), ...otherUserNotification }); + + const notifications = await storage.getNotifications({ + user, + createdAfter: new Date(Date.now() - 5 * 60 * 1000 /* 5mins */), + }); + expect(notifications.length).toBe(1); + expect(notifications.at(0)?.id).toEqual(id2); + }); }); describe('getStatus', () => { @@ -218,7 +267,6 @@ describe.each(databases.eachSupportedId())( ...testNotification, id: id1, read: new Date(), - done: new Date(), payload: { title: 'Notification', link: '/scaffolder/task/1234', @@ -242,7 +290,6 @@ describe.each(databases.eachSupportedId())( expect(existing).not.toBeNull(); expect(existing?.id).toEqual(id1); expect(existing?.payload.title).toEqual('New notification'); - expect(existing?.done).toBeNull(); expect(existing?.read).toBeNull(); }); }); @@ -283,32 +330,6 @@ describe.each(databases.eachSupportedId())( }); }); - describe('markDone', () => { - it('should mark notification done', async () => { - const id1 = uuid(); - await insertNotification({ id: id1, ...testNotification }); - - await storage.markDone({ ids: [id1], user }); - const notification = await storage.getNotification({ id: id1 }); - expect(notification?.done).not.toBeNull(); - }); - }); - - describe('markUndone', () => { - it('should mark notification undone', async () => { - const id1 = uuid(); - await insertNotification({ - id: id1, - ...testNotification, - done: new Date(), - }); - - await storage.markUndone({ ids: [id1], user }); - const notification = await storage.getNotification({ id: id1 }); - expect(notification?.done).toBeNull(); - }); - }); - describe('markSaved', () => { it('should mark notification saved', async () => { const id1 = uuid(); @@ -334,5 +355,26 @@ describe.each(databases.eachSupportedId())( expect(notification?.saved).toBeNull(); }); }); + + describe('saveNotification', () => { + it('should store a notification', async () => { + const id1 = uuid(); + await storage.saveNotification({ + id: id1, + user, + created: new Date(), + origin: 'my-origin', + payload: { + title: 'My title One', + description: 'a description of the notification', + link: 'http://foo.bar', + severity: 'normal', + topic: 'my-topic', + }, + }); + const notification = await storage.getNotification({ id: id1 }); + expect(notification?.payload?.title).toBe('My title One'); + }); + }); }, ); diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index 7e5060106b..9b654cfd04 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -61,7 +61,6 @@ export class DatabaseNotificationsStore implements NotificationsStore { id: row.id, user: row.user, created: row.created, - done: row.done, saved: row.saved, read: row.read, updated: row.updated, @@ -78,10 +77,30 @@ export class DatabaseNotificationsStore implements NotificationsStore { })); }; + private mapNotificationToDbRow = (notification: Notification) => { + return { + id: notification.id, + user: notification.user, + origin: notification.origin, + created: notification.created, + topic: notification.payload?.topic, + link: notification.payload?.link, + title: notification.payload?.title, + description: notification.payload?.description, + severity: notification.payload?.severity, + scope: notification.payload?.scope, + saved: notification.saved, + read: notification.read, + }; + }; + private getNotificationsBaseQuery = ( options: NotificationGetOptions | NotificationModifyOptions, ) => { - const { user, type } = options; + const { user } = options; + const isSQLite = this.db.client.config.client.includes('sqlite3'); + // const isPsql = this.db.client.config.client.includes('pg'); + const query = this.db('notification').where('user', user); if (options.sort !== undefined && options.sort !== null) { @@ -90,12 +109,20 @@ export class DatabaseNotificationsStore implements NotificationsStore { query.orderBy('created', options.sortOrder ?? 'desc'); } - if (type === 'undone') { - query.whereNull('done'); - } else if (type === 'done') { - query.whereNotNull('done'); - } else if (type === 'saved') { - query.whereNotNull('saved'); + if (options.createdAfter) { + if (isSQLite) { + query.where( + 'notification.created', + '>=', + options.createdAfter.valueOf(), + ); + } else { + query.where( + 'notification.created', + '>=', + options.createdAfter.toISOString(), + ); + } } if (options.limit) { @@ -117,6 +144,18 @@ export class DatabaseNotificationsStore implements NotificationsStore { query.whereIn('notification.id', options.ids); } + if (options.read) { + query.whereNotNull('notification.read'); + } else if (options.read === false) { + query.whereNull('notification.read'); + } // or match both if undefined + + if (options.saved) { + query.whereNotNull('notification.saved'); + } else if (options.saved === false) { + query.whereNull('notification.saved'); + } // or match both if undefined + return query; }; @@ -127,7 +166,9 @@ export class DatabaseNotificationsStore implements NotificationsStore { } async saveNotification(notification: Notification) { - await this.db.insert(notification).into('notification'); + await this.db + .insert(this.mapNotificationToDbRow(notification)) + .into('notification'); } async getStatus(options: NotificationGetOptions) { @@ -188,10 +229,9 @@ export class DatabaseNotificationsStore implements NotificationsStore { description: options.notification.payload.description, link: options.notification.payload.link, topic: options.notification.payload.topic, - updated: options.notification.created, + updated: new Date(), severity: options.notification.payload.severity, read: null, - done: null, }); return await this.getNotification(options); @@ -218,16 +258,6 @@ export class DatabaseNotificationsStore implements NotificationsStore { await notificationQuery.update({ read: null }); } - async markDone(options: NotificationModifyOptions): Promise { - const notificationQuery = this.getNotificationsBaseQuery(options); - await notificationQuery.update({ done: new Date(), read: new Date() }); - } - - async markUndone(options: NotificationModifyOptions): Promise { - const notificationQuery = this.getNotificationsBaseQuery(options); - await notificationQuery.update({ done: null, read: null }); - } - async markSaved(options: NotificationModifyOptions): Promise { const notificationQuery = this.getNotificationsBaseQuery(options); await notificationQuery.update({ saved: new Date() }); diff --git a/plugins/notifications-backend/src/database/NotificationsStore.ts b/plugins/notifications-backend/src/database/NotificationsStore.ts index 4e3a2fa547..0a7df92f03 100644 --- a/plugins/notifications-backend/src/database/NotificationsStore.ts +++ b/plugins/notifications-backend/src/database/NotificationsStore.ts @@ -17,19 +17,21 @@ import { Notification, NotificationStatus, - NotificationType, } from '@backstage/plugin-notifications-common'; +// TODO: reuse the common part of the type with front-end /** @internal */ export type NotificationGetOptions = { user: string; ids?: string[]; - type?: NotificationType; offset?: number; limit?: number; search?: string; sort?: 'created' | 'read' | 'updated' | null; sortOrder?: 'asc' | 'desc'; + read?: boolean; + saved?: boolean; + createdAfter?: Date; }; /** @internal */ @@ -62,10 +64,6 @@ export interface NotificationsStore { markUnread(options: NotificationModifyOptions): Promise; - markDone(options: NotificationModifyOptions): Promise; - - markUndone(options: NotificationModifyOptions): Promise; - markSaved(options: NotificationModifyOptions): Promise; markUnsaved(options: NotificationModifyOptions): Promise; diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index aa03726dfb..78d44dcd44 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -48,7 +48,6 @@ import { NewNotificationSignal, Notification, NotificationReadSignal, - NotificationType, } from '@backstage/plugin-notifications-common'; /** @internal */ @@ -190,9 +189,6 @@ export async function createRouter( const opts: NotificationGetOptions = { user: user, }; - if (req.query.type) { - opts.type = req.query.type.toString() as NotificationType; - } if (req.query.offset) { opts.offset = Number.parseInt(req.query.offset.toString(), 10); } @@ -202,6 +198,19 @@ export async function createRouter( if (req.query.search) { opts.search = req.query.search.toString(); } + if (req.query.read === 'true') { + opts.read = true; + } else if (req.query.read === 'false') { + opts.read = false; + // or keep undefined + } + if (req.query.created_after) { + const sinceEpoch = Date.parse(req.query.created_after.toString()); + if (isNaN(sinceEpoch)) { + throw new InputError('Unexpected date format'); + } + opts.createdAfter = new Date(sinceEpoch); + } const notifications = await store.getNotifications(opts); res.send(notifications); @@ -224,7 +233,7 @@ export async function createRouter( router.get('/status', async (req, res) => { const user = await getUser(req); - const status = await store.getStatus({ user, type: 'undone' }); + const status = await store.getStatus({ user }); res.send(status); }); diff --git a/plugins/notifications-common/api-report.md b/plugins/notifications-common/api-report.md index 6f40c76e58..55c9b28489 100644 --- a/plugins/notifications-common/api-report.md +++ b/plugins/notifications-common/api-report.md @@ -16,7 +16,6 @@ type Notification_2 = { created: Date; saved?: Date; read?: Date; - done?: Date; updated?: Date; origin: string; payload: NotificationPayload; @@ -27,7 +26,7 @@ export { Notification_2 as Notification }; export type NotificationPayload = { title: string; description?: string; - link: string; + link?: string; severity: NotificationSeverity; topic?: string; scope?: string; @@ -51,7 +50,4 @@ export type NotificationStatus = { unread: number; read: number; }; - -// @public (undocumented) -export type NotificationType = 'undone' | 'done' | 'saved'; ``` diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index 4d71004265..c338a772d7 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -14,9 +14,6 @@ * limitations under the License. */ -/** @public */ -export type NotificationType = 'undone' | 'done' | 'saved'; - /** @public */ export type NotificationSeverity = 'critical' | 'high' | 'normal' | 'low'; @@ -24,7 +21,7 @@ export type NotificationSeverity = 'critical' | 'high' | 'normal' | 'low'; export type NotificationPayload = { title: string; description?: string; - link: string; + link?: string; // TODO: Add support for additional links // additionalLinks?: string[]; severity: NotificationSeverity; @@ -40,7 +37,6 @@ export type Notification = { created: Date; saved?: Date; read?: Date; - done?: Date; updated?: Date; origin: string; payload: NotificationPayload; diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index 9d2094beaf..9150ce1816 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -43,6 +43,6 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-signals-node": "workspace:^", "knex": "^3.0.0", - "uuid": "^8.0.0" + "uuid": "^9.0.0" } } diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 0f5d7a0049..666daebdfe 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -12,16 +12,16 @@ import { FetchApi } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; import { Notification as Notification_2 } from '@backstage/plugin-notifications-common'; import { NotificationStatus } from '@backstage/plugin-notifications-common'; -import { NotificationType } from '@backstage/plugin-notifications-common'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export type GetNotificationsOptions = { - type?: NotificationType; offset?: number; limit?: number; search?: string; + read?: boolean; + createdAfter?: Date; }; // @public (undocumented) @@ -78,16 +78,24 @@ export const NotificationsSidebarItem: (props?: { }) => React_2.JSX.Element; // @public (undocumented) -export const NotificationsTable: (props: { - onUpdate: () => void; - type: NotificationType; +export const NotificationsTable: ({ + isLoading, + notifications, + onUpdate, + setContainsText, +}: NotificationsTableProps) => React_2.JSX.Element; + +// @public (undocumented) +export type NotificationsTableProps = { + isLoading?: boolean; notifications?: Notification_2[]; -}) => React_2.JSX.Element; + onUpdate: () => void; + setContainsText: (search: string) => void; +}; // @public (undocumented) export type UpdateNotificationsOptions = { ids: string[]; - done?: boolean; read?: boolean; saved?: boolean; }; diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index d6efb58c9e..13a565113b 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -39,6 +39,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.61", "@types/react": "^16.13.1 || ^17.0.0", + "lodash": "^4.17.21", "react-relative-time": "^0.0.9", "react-use": "^17.2.4" }, diff --git a/plugins/notifications/src/api/NotificationsApi.ts b/plugins/notifications/src/api/NotificationsApi.ts index 266c8a1ee5..4a1c792012 100644 --- a/plugins/notifications/src/api/NotificationsApi.ts +++ b/plugins/notifications/src/api/NotificationsApi.ts @@ -17,7 +17,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; import { Notification, NotificationStatus, - NotificationType, } from '@backstage/plugin-notifications-common'; /** @public */ @@ -27,16 +26,16 @@ export const notificationsApiRef = createApiRef({ /** @public */ export type GetNotificationsOptions = { - type?: NotificationType; offset?: number; limit?: number; search?: string; + read?: boolean; + createdAfter?: Date; }; /** @public */ export type UpdateNotificationsOptions = { ids: string[]; - done?: boolean; read?: boolean; saved?: boolean; }; diff --git a/plugins/notifications/src/api/NotificationsClient.test.ts b/plugins/notifications/src/api/NotificationsClient.test.ts index 1312b99cf4..daf9e71232 100644 --- a/plugins/notifications/src/api/NotificationsClient.test.ts +++ b/plugins/notifications/src/api/NotificationsClient.test.ts @@ -60,16 +60,31 @@ describe('NotificationsClient', () => { server.use( rest.get(`${mockBaseUrl}/`, (req, res, ctx) => { expect(req.url.search).toBe( - '?type=undone&limit=10&offset=0&search=find+me', + '?limit=10&offset=0&search=find+me&read=true&created_after=1970-01-01T00%3A00%3A00.005Z', ); return res(ctx.json(expectedResp)); }), ); const response = await client.getNotifications({ - type: 'undone', limit: 10, offset: 0, search: 'find me', + read: true, + createdAfter: new Date(5), + }); + expect(response).toEqual(expectedResp); + }); + + it('should omit unselected fetch options', async () => { + server.use( + rest.get(`${mockBaseUrl}/`, (req, res, ctx) => { + expect(req.url.search).toBe('?limit=10'); + return res(ctx.json(expectedResp)); + }), + ); + const response = await client.getNotifications({ + limit: 10, + // do not put more options here }); expect(response).toEqual(expectedResp); }); @@ -103,14 +118,12 @@ describe('NotificationsClient', () => { rest.post(`${mockBaseUrl}/update`, async (req, res, ctx) => { expect(await req.json()).toEqual({ ids: ['acdaa8ca-262b-43c1-b74b-de06e5f3b3c7'], - done: true, }); return res(ctx.json(expectedResp)); }), ); const response = await client.updateNotifications({ ids: ['acdaa8ca-262b-43c1-b74b-de06e5f3b3c7'], - done: true, }); expect(response).toEqual(expectedResp); }); diff --git a/plugins/notifications/src/api/NotificationsClient.ts b/plugins/notifications/src/api/NotificationsClient.ts index 9ef7b287ab..1013497b3d 100644 --- a/plugins/notifications/src/api/NotificationsClient.ts +++ b/plugins/notifications/src/api/NotificationsClient.ts @@ -42,9 +42,6 @@ export class NotificationsClient implements NotificationsApi { options?: GetNotificationsOptions, ): Promise { const queryString = new URLSearchParams(); - if (options?.type) { - queryString.append('type', options.type); - } if (options?.limit !== undefined) { queryString.append('limit', options.limit.toString(10)); } @@ -54,7 +51,12 @@ export class NotificationsClient implements NotificationsApi { if (options?.search) { queryString.append('search', options.search); } - + if (options?.read !== undefined) { + queryString.append('read', options.read ? 'true' : 'false'); + } + if (options?.createdAfter !== undefined) { + queryString.append('created_after', options.createdAfter.toISOString()); + } const urlSegment = `?${queryString}`; return await this.request(urlSegment); diff --git a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx new file mode 100644 index 0000000000..4645f46249 --- /dev/null +++ b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx @@ -0,0 +1,216 @@ +/* + * 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 React from 'react'; + +import { + Divider, + FormControl, + Grid, + InputLabel, + MenuItem, + Select, + Typography, +} from '@material-ui/core'; + +export type NotificationsFiltersProps = { + unreadOnly?: boolean; + onUnreadOnlyChanged: (checked: boolean | undefined) => void; + createdAfter?: string; + onCreatedAfterChanged: (value: string) => void; + + // sorting?: { + // orderBy: GetNotificationsOrderByEnum; + // orderByDirec: GetNotificationsOrderByDirecEnum; + // }; + // setSorting: ({ + // orderBy, + // orderByDirec, + // }: { + // orderBy: GetNotificationsOrderByEnum; + // orderByDirec: GetNotificationsOrderByDirecEnum; + // }) => void; +}; + +export const CreatedAfterOptions: { + [key: string]: { label: string; getDate: () => Date }; +} = { + last24h: { + label: 'Last 24h', + getDate: () => new Date(Date.now() - 24 * 3600 * 1000), + }, + lastWeek: { + label: 'Last week', + getDate: () => new Date(Date.now() - 7 * 24 * 3600 * 1000), + }, + all: { + label: 'Any time', + getDate: () => new Date(0), + }, +}; + +// export const SortByOptions: { +// [key: string]: { +// label: string; +// orderBy: GetNotificationsOrderByEnum; +// orderByDirec: GetNotificationsOrderByDirecEnum; +// }; +// } = { +// newest: { +// label: 'Newest on top', +// orderBy: GetNotificationsOrderByEnum.Created, +// orderByDirec: GetNotificationsOrderByDirecEnum.Asc, +// }, +// oldest: { +// label: 'Oldest on top', +// orderBy: GetNotificationsOrderByEnum.Created, +// orderByDirec: GetNotificationsOrderByDirecEnum.Desc, +// }, +// topic: { +// label: 'Topic', +// orderBy: GetNotificationsOrderByEnum.Topic, +// orderByDirec: GetNotificationsOrderByDirecEnum.Asc, +// }, +// origin: { +// label: 'Origin', +// orderBy: GetNotificationsOrderByEnum.Origin, +// orderByDirec: GetNotificationsOrderByDirecEnum.Asc, +// }, +// }; + +// TODO: Implement sorting on server (to work with pagination) +// const getSortBy = (sorting: NotificationsFiltersProps['sorting']): string => { +// if ( +// sorting?.orderBy === GetNotificationsOrderByEnum.Created && +// sorting.orderByDirec === GetNotificationsOrderByDirecEnum.Desc +// ) { +// return 'oldest'; +// } +// if (sorting?.orderBy === GetNotificationsOrderByEnum.Topic) { +// return 'topic'; +// } +// if (sorting?.orderBy === GetNotificationsOrderByEnum.Origin) { +// return 'origin'; +// } + +// return 'newest'; +// }; + +export const NotificationsFilters = ({ + // sorting, + // setSorting, + unreadOnly, + onUnreadOnlyChanged, + createdAfter, + onCreatedAfterChanged, +}: NotificationsFiltersProps) => { + // const sortBy = getSortBy(sorting); + + const handleOnCreatedAfterChanged = ( + event: React.ChangeEvent<{ name?: string; value: unknown }>, + ) => { + onCreatedAfterChanged(event.target.value as string); + }; + + const handleOnUnreadOnlyChanged = ( + event: React.ChangeEvent<{ name?: string; value: unknown }>, + ) => { + let value = undefined; + if (event.target.value === 'unread') value = true; + if (event.target.value === 'read') value = false; + onUnreadOnlyChanged(value); + }; + + // const handleOnSortByChanged = ( + // event: React.ChangeEvent<{ name?: string; value: unknown }>, + // ) => { + // const idx = (event.target.value as string) || 'newest'; + // const option = SortByOptions[idx]; + // setSorting({ + // orderBy: option.orderBy, + // orderByDirec: option.orderByDirec, + // }); + // }; + + let unreadOnlyValue = 'all'; + if (unreadOnly) unreadOnlyValue = 'unread'; + if (unreadOnly === false) unreadOnlyValue = 'read'; + + return ( + <> + + + Filters + + + + + View + + + + + + + Created after + + + + + + + {/* + + + Sort by + + + + */} + + + ); +}; diff --git a/plugins/notifications/src/components/NotificationsFilters/index.ts b/plugins/notifications/src/components/NotificationsFilters/index.ts new file mode 100644 index 0000000000..196aef61e0 --- /dev/null +++ b/plugins/notifications/src/components/NotificationsFilters/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './NotificationsFilters'; diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx index 85e0937ca3..1f8b141608 100644 --- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx +++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx @@ -14,35 +14,46 @@ * limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { Content, - ErrorPanel, PageWithHeader, + ResponseErrorPanel, } from '@backstage/core-components'; -import { NotificationsTable } from '../NotificationsTable'; -import { useNotificationsApi } from '../../hooks'; -import { Button, Grid, makeStyles } from '@material-ui/core'; -import Bookmark from '@material-ui/icons/Bookmark'; -import Check from '@material-ui/icons/Check'; -import Inbox from '@material-ui/icons/Inbox'; -import { NotificationType } from '@backstage/plugin-notifications-common'; +import { Grid } from '@material-ui/core'; import { useSignal } from '@backstage/plugin-signals-react'; -const useStyles = makeStyles(_theme => ({ - filterButton: { - width: '100%', - justifyContent: 'start', - }, -})); +import { NotificationsTable } from '../NotificationsTable'; +import { useNotificationsApi } from '../../hooks'; +import { + CreatedAfterOptions, + NotificationsFilters, +} from '../NotificationsFilters'; +import { GetNotificationsOptions } from '../../api'; export const NotificationsPage = () => { - const [type, setType] = useState('undone'); const [refresh, setRefresh] = React.useState(false); + const { lastSignal } = useSignal('notifications'); + const [unreadOnly, setUnreadOnly] = React.useState(true); + const [containsText, setContainsText] = React.useState(); + const [createdAfter, setCreatedAfter] = React.useState('lastWeek'); - const { error, value, retry } = useNotificationsApi( - api => api.getNotifications({ type }), - [type], + const { error, value, retry, loading } = useNotificationsApi( + // TODO: add pagination and other filters + api => { + const options: GetNotificationsOptions = { search: containsText }; + if (unreadOnly !== undefined) { + options.read = !unreadOnly; + } + + const createdAfterDate = CreatedAfterOptions[createdAfter].getDate(); + if (createdAfterDate.valueOf() > 0) { + options.createdAfter = createdAfterDate; + } + + return api.getNotifications(options); + }, + [containsText, unreadOnly, createdAfter], ); useEffect(() => { @@ -52,7 +63,6 @@ export const NotificationsPage = () => { } }, [refresh, setRefresh, retry]); - const { lastSignal } = useSignal('notifications'); useEffect(() => { if (lastSignal && lastSignal.action) { setRefresh(true); @@ -63,9 +73,8 @@ export const NotificationsPage = () => { setRefresh(true); }; - const styles = useStyles(); if (error) { - return ; + return ; } return ( @@ -73,36 +82,21 @@ export const NotificationsPage = () => { - - - + diff --git a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx index 8328da1c6d..6bcb6badc7 100644 --- a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx +++ b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx @@ -13,296 +13,162 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useEffect, useState } from 'react'; -import { - Box, - Button, - IconButton, - makeStyles, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Tooltip, - Typography, -} from '@material-ui/core'; -import { - Notification, - NotificationType, -} from '@backstage/plugin-notifications-common'; -import { useNavigate } from 'react-router-dom'; -import Checkbox from '@material-ui/core/Checkbox'; -import Check from '@material-ui/icons/Check'; -import Bookmark from '@material-ui/icons/Bookmark'; +import React, { useMemo } from 'react'; +import throttle from 'lodash/throttle'; +import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'; +import { Notification } from '@backstage/plugin-notifications-common'; import { notificationsApiRef } from '../../api'; import { useApi } from '@backstage/core-plugin-api'; -import Inbox from '@material-ui/icons/Inbox'; -import CloseIcon from '@material-ui/icons/Close'; +import MarkAsUnreadIcon from '@material-ui/icons/Markunread'; +import MarkAsReadIcon from '@material-ui/icons/CheckCircle'; + // @ts-ignore import RelativeTime from 'react-relative-time'; -import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; +import { Link, Table, TableColumn } from '@backstage/core-components'; -const useStyles = makeStyles(theme => ({ - table: { - border: `1px solid ${theme.palette.divider}`, - }, - header: { - borderBottom: `1px solid ${theme.palette.divider}`, - }, - - notificationRow: { - cursor: 'pointer', - '&.unread': { - border: '1px solid rgba(255, 255, 255, .3)', - }, - '& .hideOnHover': { - display: 'initial', - }, - '& .showOnHover': { - display: 'none', - }, - '&:hover': { - '& .hideOnHover': { - display: 'none', - }, - '& .showOnHover': { - display: 'initial', - }, - }, - }, - actionButton: { - padding: '9px', - }, - checkBox: { - padding: '0 10px 10px 0', - }, -})); +const ThrottleDelayMs = 1000; /** @public */ -export const NotificationsTable = (props: { - onUpdate: () => void; - type: NotificationType; +export type NotificationsTableProps = { + isLoading?: boolean; notifications?: Notification[]; -}) => { - const { notifications, type } = props; - const navigate = useNavigate(); - const styles = useStyles(); - const [selected, setSelected] = useState([]); + onUpdate: () => void; + setContainsText: (search: string) => void; +}; + +/** @public */ +export const NotificationsTable = ({ + isLoading, + notifications = [], + onUpdate, + setContainsText, +}: NotificationsTableProps) => { const notificationsApi = useApi(notificationsApiRef); - const onCheckBoxClick = (id: string) => { - const index = selected.indexOf(id); - if (index !== -1) { - setSelected(selected.filter(s => s !== id)); - } else { - setSelected([...selected, id]); - } - }; + const onSwitchReadStatus = React.useCallback( + (notification: Notification) => { + notificationsApi + .updateNotifications({ + ids: [notification.id], + read: !notification.read, + }) + .then(() => onUpdate()); + }, + [notificationsApi, onUpdate], + ); - useEffect(() => { - setSelected([]); - }, [type]); + const throttledContainsTextHandler = useMemo( + () => throttle(setContainsText, ThrottleDelayMs), + [setContainsText], + ); - const isChecked = (id: string) => { - return selected.indexOf(id) !== -1; - }; - - const isAllSelected = () => { - return ( - selected.length === notifications?.length && notifications.length > 0 - ); - }; - - return ( - - - - - {type !== 'saved' && !notifications?.length && 'No notifications'} - {type !== 'saved' && !!notifications?.length && ( - { - if (isAllSelected()) { - setSelected([]); - } else { - setSelected( - notifications ? notifications.map(n => n.id) : [], - ); - } - }} - /> - )} - {type === 'saved' && - `${notifications?.length ?? 0} saved notifications`} - {selected.length === 0 && - !!notifications?.length && - type !== 'saved' && - 'Select all'} - {selected.length > 0 && `${selected.length} selected`} - {type === 'done' && selected.length > 0 && ( - - )} - - {type === 'undone' && selected.length > 0 && ( - - )} - - - - - {props.notifications?.map(notification => { + const compactColumns = React.useMemo( + (): TableColumn[] => [ + { + customFilterAndSearch: () => + true /* Keep it on backend due to pagination. If recent flickering is an issue, implement search here as well. */, + render: (notification: Notification) => { + // Compact content return ( - - - onCheckBoxClick(notification.id)} - /> - - - notificationsApi - .updateNotifications({ ids: [notification.id], read: true }) - .then(() => navigate(notification.payload.link)) - } - style={{ paddingLeft: 0 }} - > + <> + - {notification.payload.title} + {notification.payload.link ? ( + + {notification.payload.title} + + ) : ( + notification.payload.title + )} {notification.payload.description} - - - - - - - - - notificationsApi - .updateNotifications({ - ids: [notification.id], - read: true, - }) - .then(() => navigate(notification.payload.link)) - } - > - - - - - { - if (notification.read) { - notificationsApi - .updateNotifications({ - ids: [notification.id], - done: false, - }) - .then(() => { - props.onUpdate(); - }); - } else { - notificationsApi - .updateNotifications({ - ids: [notification.id], - done: true, - }) - .then(() => { - props.onUpdate(); - }); - } - }} - > - {notification.read ? ( - - ) : ( - - )} - - - - { - if (notification.saved) { - notificationsApi - .updateNotifications({ - ids: [notification.id], - saved: false, - }) - .then(() => { - props.onUpdate(); - }); - } else { - notificationsApi - .updateNotifications({ - ids: [notification.id], - saved: true, - }) - .then(() => { - props.onUpdate(); - }); - } - }} - > - {notification.saved ? ( - - ) : ( - - )} - - - - - + + {notification.origin && ( + <>{notification.origin} •  + )} + {notification.payload.topic && ( + <>{notification.payload.topic} •  + )} + {notification.created && ( + + )} + + + ); - })} - -
+ }, + }, + // { + // // TODO: additional action links + // width: '25%', + // render: (notification: Notification) => { + // return ( + // notification.payload.link && ( + // + // {/* TODO: render additionalLinks of different titles */} + // + // + //  More info + // + // + // + // ) + // ); + // }, + // }, + { + // TODO: action for saving notifications + // actions + width: '1rem', + render: (notification: Notification) => { + const markAsReadText = !!notification.read + ? 'Return among unread' + : 'Mark as read'; + const IconComponent = !!notification.read + ? MarkAsUnreadIcon + : MarkAsReadIcon; + + return ( + + { + onSwitchReadStatus(notification); + }} + > + + + + ); + }, + }, + ], + [onSwitchReadStatus], + ); + + // TODO: render "Saved notifications" as "Pinned" + return ( + + isLoading={isLoading} + options={{ + search: true, + // TODO: add pagination + // paging: true, + // pageSize, + header: false, + sorting: false, + }} + // onPageChange={setPageNumber} + // onRowsPerPageChange={setPageSize} + // page={offset} + // totalCount={value?.totalCount} + onSearchChange={throttledContainsTextHandler} + data={notifications} + columns={compactColumns} + /> ); }; diff --git a/plugins/permission-backend/api-report.md b/plugins/permission-backend/api-report.md index e9cc4e6272..8b4336ff23 100644 --- a/plugins/permission-backend/api-report.md +++ b/plugins/permission-backend/api-report.md @@ -3,27 +3,36 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { Logger } from 'winston'; import { PermissionPolicy } from '@backstage/plugin-permission-node'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { UserInfoService } from '@backstage/backend-plugin-api'; // @public export function createRouter(options: RouterOptions): Promise; // @public export interface RouterOptions { + // (undocumented) + auth?: AuthService; // (undocumented) config: Config; // (undocumented) - discovery: PluginEndpointDiscovery; + discovery: DiscoveryService; // (undocumented) - identity: IdentityApi; + httpAuth?: HttpAuthService; + // (undocumented) + identity?: IdentityApi; // (undocumented) logger: Logger; // (undocumented) policy: PermissionPolicy; + // (undocumented) + userInfo?: UserInfoService; } ``` diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index cc986c4cc8..4131dc29b9 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -62,6 +62,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", diff --git a/plugins/permission-backend/src/plugin.ts b/plugins/permission-backend/src/plugin.ts index 9cf0024e47..cc7f31dfdd 100644 --- a/plugins/permission-backend/src/plugin.ts +++ b/plugins/permission-backend/src/plugin.ts @@ -55,9 +55,19 @@ export const permissionPlugin = createBackendPlugin({ config: coreServices.rootConfig, logger: coreServices.logger, discovery: coreServices.discovery, - identity: coreServices.identity, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + userInfo: coreServices.userInfo, }, - async init({ http, config, logger, discovery, identity }) { + async init({ + http, + config, + logger, + discovery, + auth, + httpAuth, + userInfo, + }) { const winstonLogger = loggerToWinstonLogger(logger); if (!policies.policy) { throw new Error( @@ -69,9 +79,11 @@ export const permissionPlugin = createBackendPlugin({ await createRouter({ config, discovery, - identity, logger: winstonLogger, policy: policies.policy, + auth, + httpAuth, + userInfo, }), ); }, diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts index c7a412547d..a36dadd2b5 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts @@ -19,7 +19,7 @@ import { Server } from 'http'; import express, { Router, RequestHandler } from 'express'; import { RestContext, rest } from 'msw'; import { setupServer, SetupServer } from 'msw/node'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; import { AuthorizeResult, PermissionCondition, @@ -31,10 +31,12 @@ import { } from '@backstage/plugin-permission-node'; import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { z } from 'zod'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; describe('PermissionIntegrationClient', () => { describe('applyConditions', () => { let server: SetupServer; + const auth = mockServices.auth(); const mockConditions: PermissionCriteria = { not: { @@ -58,7 +60,7 @@ describe('PermissionIntegrationClient', () => { ); const mockBaseUrl = 'http://backstage:9191'; - const discovery: PluginEndpointDiscovery = { + const discovery: DiscoveryService = { async getBaseUrl(pluginId) { return `${mockBaseUrl}/${pluginId}`; }, @@ -70,6 +72,7 @@ describe('PermissionIntegrationClient', () => { const client: PermissionIntegrationClient = new PermissionIntegrationClient( { discovery, + auth, }, ); @@ -91,7 +94,7 @@ describe('PermissionIntegrationClient', () => { }); it('should make a POST request to the correct endpoint', async () => { - await client.applyConditions('plugin-1', [ + await client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -104,7 +107,7 @@ describe('PermissionIntegrationClient', () => { }); it('should include a request body', async () => { - await client.applyConditions('plugin-1', [ + await client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -132,14 +135,18 @@ describe('PermissionIntegrationClient', () => { }); it('should return the response from the fetch request', async () => { - const response = await client.applyConditions('plugin-1', [ - { - id: '123', - resourceRef: 'testResource1', - resourceType: 'test-resource', - conditions: mockConditions, - }, - ]); + const response = await client.applyConditions( + 'plugin-1', + mockCredentials.none(), + [ + { + id: '123', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }, + ], + ); expect(response).toEqual( expect.objectContaining([{ id: '123', result: AuthorizeResult.ALLOW }]), @@ -147,7 +154,7 @@ describe('PermissionIntegrationClient', () => { }); it('should not include authorization headers if no token is supplied', async () => { - await client.applyConditions('plugin-1', [ + await client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -161,21 +168,22 @@ describe('PermissionIntegrationClient', () => { }); it('should include correctly-constructed authorization header if token is supplied', async () => { - await client.applyConditions( - 'plugin-1', - [ - { - id: '123', - resourceRef: 'testResource1', - resourceType: 'test-resource', - conditions: mockConditions, - }, - ], - 'Bearer fake-token', - ); + await client.applyConditions('plugin-1', mockCredentials.user(), [ + { + id: '123', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }, + ]); const request = mockApplyConditionsHandler.mock.calls[0][0]; - expect(request.headers.get('authorization')).toEqual('Bearer fake-token'); + expect(request.headers.get('authorization')).toEqual( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'plugin-1', + }), + ); }); it('should forward response errors', async () => { @@ -186,7 +194,7 @@ describe('PermissionIntegrationClient', () => { ); await expect( - client.applyConditions('plugin-1', [ + client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -194,7 +202,7 @@ describe('PermissionIntegrationClient', () => { conditions: mockConditions, }, ]), - ).rejects.toThrow(/401/i); + ).rejects.toThrow(/401/); }); it('should reject invalid responses', async () => { @@ -207,7 +215,7 @@ describe('PermissionIntegrationClient', () => { ); await expect( - client.applyConditions('plugin-1', [ + client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -234,7 +242,7 @@ describe('PermissionIntegrationClient', () => { ); await expect( - client.applyConditions('plugin-1', [ + client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -268,6 +276,7 @@ describe('PermissionIntegrationClient', () => { let server: Server; let client: PermissionIntegrationClient; let routerSpy: RequestHandler; + const auth = mockServices.auth(); beforeAll(async () => { const router = Router(); @@ -319,7 +328,7 @@ describe('PermissionIntegrationClient', () => { server = app.listen(resolve); }); - const discovery: PluginEndpointDiscovery = { + const discovery: DiscoveryService = { async getBaseUrl(pluginId: string) { const listenPort = (server.address()! as AddressInfo).port; @@ -332,6 +341,7 @@ describe('PermissionIntegrationClient', () => { client = new PermissionIntegrationClient({ discovery, + auth, }); }); @@ -348,7 +358,7 @@ describe('PermissionIntegrationClient', () => { it('works for simple conditions', async () => { await expect( - client.applyConditions('plugin-1', [ + client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -367,7 +377,7 @@ describe('PermissionIntegrationClient', () => { it('works for complex criteria', async () => { await expect( - client.applyConditions('plugin-1', [ + client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts index 2c31d371c3..7567dc8fbb 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts @@ -16,7 +16,6 @@ import fetch from 'node-fetch'; import { z } from 'zod'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { AuthorizeResult, ConditionalPolicyDecision, @@ -25,6 +24,11 @@ import { ApplyConditionsRequestEntry, ApplyConditionsResponseEntry, } from '@backstage/plugin-permission-node'; +import { + AuthService, + BackstageCredentials, + DiscoveryService, +} from '@backstage/backend-plugin-api'; const responseSchema = z.object({ items: z.array( @@ -42,20 +46,30 @@ export type ResourcePolicyDecision = ConditionalPolicyDecision & { }; export class PermissionIntegrationClient { - private readonly discovery: PluginEndpointDiscovery; + private readonly discovery: DiscoveryService; + private readonly auth: AuthService; - constructor(options: { discovery: PluginEndpointDiscovery }) { + constructor(options: { discovery: DiscoveryService; auth: AuthService }) { this.discovery = options.discovery; + this.auth = options.auth; } async applyConditions( pluginId: string, + credentials: BackstageCredentials, decisions: readonly ApplyConditionsRequestEntry[], - authHeader?: string, ): Promise { - const endpoint = `${await this.discovery.getBaseUrl( - pluginId, - )}/.well-known/backstage/permissions/apply-conditions`; + const baseUrl = await this.discovery.getBaseUrl(pluginId); + const endpoint = `${baseUrl}/.well-known/backstage/permissions/apply-conditions`; + + const token = this.auth.isPrincipal(credentials, 'none') + ? undefined + : await this.auth + .getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: pluginId, + }) + .then(t => t.token); const response = await fetch(endpoint, { method: 'POST', @@ -70,7 +84,7 @@ export class PermissionIntegrationClient { ), }), headers: { - ...(authHeader ? { authorization: authHeader } : {}), + ...(token ? { authorization: `Bearer ${token}` } : {}), 'content-type': 'application/json', }, }); diff --git a/plugins/permission-backend/src/service/router.test.ts b/plugins/permission-backend/src/service/router.test.ts index 7278835c5c..0da9ecd748 100644 --- a/plugins/permission-backend/src/service/router.test.ts +++ b/plugins/permission-backend/src/service/router.test.ts @@ -26,12 +26,15 @@ import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { createRouter } from './router'; import { ConfigReader } from '@backstage/config'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; const mockApplyConditions: jest.MockedFunction< InstanceType['applyConditions'] > = jest.fn( async ( _pluginId: string, + _credentials: BackstageCredentials, decisions: readonly ApplyConditionsRequestEntry[], ) => decisions.map(decision => ({ @@ -65,28 +68,12 @@ describe('createRouter', () => { const router = await createRouter({ config: new ConfigReader({ permission: { enabled: true } }), logger: getVoidLogger(), - discovery: { - getBaseUrl: jest.fn(), - getExternalBaseUrl: jest.fn(), - }, - identity: { - getIdentity: jest.fn(({ request: req }) => { - const token = req.headers.authorization?.replace(/^Bearer[ ]+/, ''); - - if (!token) { - return Promise.resolve(undefined); - } - - return Promise.resolve({ - identity: { - type: 'user', - userEntityRef: 'test-user', - ownershipEntityRefs: ['blah'], - }, - token, - }); - }), - }, + discovery: mockServices.discovery(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth({ + defaultCredentials: mockCredentials.none(), + }), + userInfo: mockServices.userInfo(), policy, }); @@ -163,10 +150,9 @@ describe('createRouter', () => { }); it('resolves identity from the Authorization header', async () => { - const token = 'test-token'; const response = await request(app) .post('/authorize') - .auth(token, { type: 'bearer' }) + .auth(mockCredentials.user.token(), { type: 'bearer' }) .send({ items: [ { @@ -190,11 +176,16 @@ describe('createRouter', () => { }, }, { - token: 'test-token', + token: mockCredentials.service.token({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'catalog', + }), identity: { type: 'user', - userEntityRef: 'test-user', - ownershipEntityRefs: ['blah'], + userEntityRef: mockCredentials.user().principal.userEntityRef, + ownershipEntityRefs: [ + mockCredentials.user().principal.userEntityRef, + ], }, }, ); @@ -271,7 +262,7 @@ describe('createRouter', () => { const response = await request(app) .post('/authorize') - .auth('test-token', { type: 'bearer' }) + .auth(mockCredentials.user.token(), { type: 'bearer' }) .send({ items: [ { @@ -319,6 +310,7 @@ describe('createRouter', () => { expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-1', + mockCredentials.user(), [ expect.objectContaining({ id: '123', @@ -333,11 +325,11 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['no'] }, }), ], - 'Bearer test-token', ); expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-2', + mockCredentials.user(), [ expect.objectContaining({ id: '234', @@ -352,7 +344,6 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['no'] }, }), ], - 'Bearer test-token', ); expect(response.status).toEqual(200); @@ -401,7 +392,7 @@ describe('createRouter', () => { const response = await request(app) .post('/authorize') - .auth('test-token', { type: 'bearer' }) + .auth(mockCredentials.user.token(), { type: 'bearer' }) .send({ items: [ { @@ -467,6 +458,7 @@ describe('createRouter', () => { expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-1', + mockCredentials.user(), [ expect.objectContaining({ id: '123', @@ -481,11 +473,11 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['yes'] }, }), ], - 'Bearer test-token', ); expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-2', + mockCredentials.user(), [ expect.objectContaining({ id: '234', @@ -500,7 +492,6 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['yes'] }, }), ], - 'Bearer test-token', ); expect(response.status).toEqual(200); @@ -542,7 +533,7 @@ describe('createRouter', () => { const response = await request(app) .post('/authorize') - .auth('test-token', { type: 'bearer' }) + .auth(mockCredentials.user.token(), { type: 'bearer' }) .send({ items: [ { @@ -589,6 +580,7 @@ describe('createRouter', () => { expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-1', + mockCredentials.user(), [ expect.objectContaining({ id: '123', @@ -597,11 +589,11 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['yes'] }, }), ], - 'Bearer test-token', ); expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-2', + mockCredentials.user(), [ expect.objectContaining({ id: '234', @@ -610,7 +602,6 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['yes'] }, }), ], - 'Bearer test-token', ); expect(response.status).toEqual(200); @@ -656,7 +647,7 @@ describe('createRouter', () => { const response = await request(app) .post('/authorize') - .auth('test-token', { type: 'bearer' }) + .auth(mockCredentials.user.token(), { type: 'bearer' }) .send({ items: [ { @@ -684,6 +675,7 @@ describe('createRouter', () => { expect(mockApplyConditions).toHaveBeenCalledWith( 'test-plugin', + mockCredentials.user(), [ expect.objectContaining({ id: '123', @@ -698,7 +690,6 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params }, }), ], - 'Bearer test-token', ); expect(response.status).toEqual(200); diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index b7e77fdba9..cd7c71fd87 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -19,8 +19,8 @@ import express, { Request, Response } from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { + createLegacyAuthAdapters, errorHandler, - PluginEndpointDiscovery, } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import { @@ -46,6 +46,15 @@ import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { memoize } from 'lodash'; import DataLoader from 'dataloader'; import { Config } from '@backstage/config'; +import { + AuthService, + BackstageCredentials, + BackstageNonePrincipal, + BackstageUserPrincipal, + DiscoveryService, + HttpAuthService, + UserInfoService, +} from '@backstage/backend-plugin-api'; const attributesSchema: z.ZodSchema = z.object({ action: z @@ -93,28 +102,51 @@ const evaluatePermissionRequestBatchSchema: z.ZodSchema[], - user: BackstageIdentityResponse | undefined, policy: PermissionPolicy, permissionIntegrationClient: PermissionIntegrationClient, - authHeader?: string, + credentials: BackstageCredentials< + BackstageNonePrincipal | BackstageUserPrincipal + >, + auth: AuthService, + userInfo: UserInfoService, ): Promise[]> => { const applyConditionsLoaderFor = memoize((pluginId: string) => { return new DataLoader< ApplyConditionsRequestEntry, ApplyConditionsResponseEntry >(batch => - permissionIntegrationClient.applyConditions(pluginId, batch, authHeader), + permissionIntegrationClient.applyConditions(pluginId, credentials, batch), ); }); + let user: BackstageIdentityResponse | undefined; + if (auth.isPrincipal(credentials, 'user')) { + const { ownershipEntityRefs } = await userInfo.getUserInfo(credentials); + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'catalog', // TODO: unknown at this point + }); + user = { + identity: { + type: 'user', + userEntityRef: credentials.principal.userEntityRef, + ownershipEntityRefs, + }, + token, + }; + } + return Promise.all( requests.map(({ id, resourceRef, ...request }) => policy.handle(request, user).then(decision => { @@ -163,7 +195,8 @@ const handleRequest = async ( export async function createRouter( options: RouterOptions, ): Promise { - const { policy, discovery, identity, config, logger } = options; + const { policy, discovery, config, logger } = options; + const { auth, httpAuth, userInfo } = createLegacyAuthAdapters(options); if (!config.getOptionalBoolean('permission.enabled')) { logger.warn( @@ -173,6 +206,7 @@ export async function createRouter( const permissionIntegrationClient = new PermissionIntegrationClient({ discovery, + auth, }); const router = Router(); @@ -188,7 +222,9 @@ export async function createRouter( req: Request, res: Response, ) => { - const user = await identity.getIdentity({ request: req }); + const credentials = await httpAuth.credentials(req, { + allow: ['user', 'none'], + }); const parseResult = evaluatePermissionRequestBatchSchema.safeParse( req.body, @@ -203,10 +239,11 @@ export async function createRouter( res.json({ items: await handleRequest( body.items, - user, policy, permissionIntegrationClient, - req.header('authorization'), + credentials, + auth, + userInfo, ), }); }, diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 7dd86b3b0d..204e146e56 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -45,7 +45,7 @@ "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "cross-fetch": "^4.0.0", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "zod": "^3.22.4" }, "devDependencies": { diff --git a/plugins/playlist-backend/api-report.md b/plugins/playlist-backend/api-report.md index 40b05c25d4..0a6aa3e3bf 100644 --- a/plugins/playlist-backend/api-report.md +++ b/plugins/playlist-backend/api-report.md @@ -3,19 +3,21 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-common'; import { Conditions } from '@backstage/plugin-permission-node'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionCondition } from '@backstage/plugin-permission-common'; import { PermissionCriteria } from '@backstage/plugin-permission-common'; -import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PermissionPolicy } from '@backstage/plugin-permission-node'; import { PermissionRule } from '@backstage/plugin-permission-node'; +import { PermissionsService } from '@backstage/backend-plugin-api'; import { PlaylistMetadata } from '@backstage/plugin-playlist-common'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -87,15 +89,19 @@ export default playlistPlugin; // @public (undocumented) export interface RouterOptions { + // (undocumented) + auth?: AuthService; // (undocumented) database: PluginDatabaseManager; // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) + httpAuth?: HttpAuthService; + // (undocumented) identity: IdentityApi; // (undocumented) logger: Logger; // (undocumented) - permissions: PermissionEvaluator; + permissions: PermissionsService; } ``` diff --git a/plugins/playlist-backend/package.json b/plugins/playlist-backend/package.json index af26e90102..e7889830e1 100644 --- a/plugins/playlist-backend/package.json +++ b/plugins/playlist-backend/package.json @@ -47,7 +47,7 @@ "express-promise-router": "^4.1.0", "knex": "^3.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.2.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0", "zod": "^3.22.4" diff --git a/plugins/playlist-backend/src/service/DatabaseHandler.ts b/plugins/playlist-backend/src/service/DatabaseHandler.ts index 6118f22ff9..afcaf92d24 100644 --- a/plugins/playlist-backend/src/service/DatabaseHandler.ts +++ b/plugins/playlist-backend/src/service/DatabaseHandler.ts @@ -15,7 +15,7 @@ */ import { resolvePackagePath } from '@backstage/backend-common'; -import { BackstageUserIdentity } from '@backstage/plugin-auth-node'; +import { BackstageUserPrincipal } from '@backstage/backend-plugin-api'; import { Playlist, PlaylistMetadata } from '@backstage/plugin-playlist-common'; import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; @@ -108,7 +108,7 @@ export class DatabaseHandler { private playlistColumns = ['id', 'name', 'description', 'owner', 'public']; async listPlaylists( - user: BackstageUserIdentity, + user: BackstageUserPrincipal, filter?: ListPlaylistsFilter, ): Promise { let playlistQuery = this.database>( @@ -177,7 +177,7 @@ export class DatabaseHandler { async getPlaylist( id: string, - user?: BackstageUserIdentity, + user?: BackstageUserPrincipal, ): Promise { const playlist = await this.database>( 'playlists', @@ -263,14 +263,14 @@ export class DatabaseHandler { .del(); } - async followPlaylist(playlistId: string, user: BackstageUserIdentity) { + async followPlaylist(playlistId: string, user: BackstageUserPrincipal) { await this.database('followers') .insert({ playlist_id: playlistId, user_ref: user.userEntityRef }) .onConflict(['playlist_id', 'user_ref']) .ignore(); } - async unfollowPlaylist(playlistId: string, user: BackstageUserIdentity) { + async unfollowPlaylist(playlistId: string, user: BackstageUserPrincipal) { await this.database('followers') .where({ playlist_id: playlistId, user_ref: user.userEntityRef }) .del(); diff --git a/plugins/playlist-backend/src/service/router.test.ts b/plugins/playlist-backend/src/service/router.test.ts index 3f90a8b8af..d28eae9041 100644 --- a/plugins/playlist-backend/src/service/router.test.ts +++ b/plugins/playlist-backend/src/service/router.test.ts @@ -20,13 +20,13 @@ import { PluginEndpointDiscovery, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { IdentityApi } from '@backstage/plugin-auth-node'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { permissions } from '@backstage/plugin-playlist-common'; import express from 'express'; import request from 'supertest'; import { createRouter } from './router'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; const sampleEntities = [ { @@ -60,10 +60,6 @@ jest.mock('@backstage/catalog-client', () => ({ .mockImplementation(() => ({ getEntities: mockGetEntties })), })); -jest.mock('@backstage/plugin-auth-node', () => ({ - getBearerTokenFromAuthorizationHeader: () => 'token', -})); - const mockConditionFilter = { key: 'test', values: ['test-val'] }; jest.mock('../permissions', () => ({ ...jest.requireActual('../permissions'), @@ -128,17 +124,6 @@ describe('createRouter', () => { authorizeConditional: mockedAuthorizeConditional, }; - const mockUser = { - type: 'user', - ownershipEntityRefs: ['user:default/me', 'group:default/owner'], - userEntityRef: 'user:default/me', - }; - const mockIdentityClient = { - getIdentity: jest - .fn() - .mockImplementation(async () => ({ identity: mockUser })), - } as unknown as IdentityApi; - const discovery: jest.Mocked = { getBaseUrl: jest.fn(), getExternalBaseUrl: jest.fn(), @@ -148,9 +133,11 @@ describe('createRouter', () => { const router = await createRouter({ database: createDatabase(), discovery, - identity: mockIdentityClient, + identity: mockServices.identity(), logger: getVoidLogger(), permissions: mockPermissionEvaluator, + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = express().use(router); @@ -173,7 +160,7 @@ describe('createRouter', () => { expect(mockedAuthorizeConditional).toHaveBeenCalledWith( [{ permission: permissions.playlistListRead }], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.listPlaylists).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -182,7 +169,7 @@ describe('createRouter', () => { it('should get playlists correctly', async () => { let response = await request(app).get('/').send(); expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( - mockUser, + mockCredentials.user().principal, undefined, ); expect(response.status).toEqual(200); @@ -193,7 +180,7 @@ describe('createRouter', () => { ]); response = await request(app).get('/').send(); expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( - mockUser, + mockCredentials.user().principal, mockConditionFilter, ); expect(response.status).toEqual(200); @@ -205,7 +192,7 @@ describe('createRouter', () => { .get('/?filter=mock=test&filter=foo=bar') .send(); expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( - mockUser, + mockCredentials.user().principal, mockRequestFilter, ); expect(response.status).toEqual(200); @@ -217,9 +204,12 @@ describe('createRouter', () => { response = await request(app) .get('/?filter=mock=test&filter=foo=bar') .send(); - expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith(mockUser, { - allOf: [mockRequestFilter, mockConditionFilter], - }); + expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( + mockCredentials.user().principal, + { + allOf: [mockRequestFilter, mockConditionFilter], + }, + ); expect(response.status).toEqual(200); expect(response.body).toEqual([mockPlaylist]); }); @@ -228,10 +218,10 @@ describe('createRouter', () => { let response = await request(app).get('/?editable=true').send(); expect(mockedAuthorizeConditional).toHaveBeenCalledWith( [{ permission: permissions.playlistListUpdate }], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( - mockUser, + mockCredentials.user().principal, undefined, ); expect(response.status).toEqual(200); @@ -241,7 +231,7 @@ describe('createRouter', () => { .get('/?editable=true&filter=mock=test&filter=foo=bar') .send(); expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( - mockUser, + mockCredentials.user().principal, mockRequestFilter, ); expect(response.status).toEqual(200); @@ -251,9 +241,10 @@ describe('createRouter', () => { { result: AuthorizeResult.CONDITIONAL }, ]); response = await request(app).get('/?editable=true').send(); - expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith(mockUser, { - allOf: [mockConditionFilter, mockConditionFilter], - }); + expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( + mockCredentials.user().principal, + { allOf: [mockConditionFilter, mockConditionFilter] }, + ); expect(response.status).toEqual(200); expect(response.body).toEqual([mockPlaylist]); @@ -263,12 +254,15 @@ describe('createRouter', () => { response = await request(app) .get('/?editable=true&filter=mock=test&filter=foo=bar') .send(); - expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith(mockUser, { - allOf: [ - { allOf: [mockRequestFilter, mockConditionFilter] }, - mockConditionFilter, - ], - }); + expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( + mockCredentials.user().principal, + { + allOf: [ + { allOf: [mockRequestFilter, mockConditionFilter] }, + mockConditionFilter, + ], + }, + ); expect(response.status).toEqual(200); expect(response.body).toEqual([mockPlaylist]); }); @@ -285,7 +279,7 @@ describe('createRouter', () => { expect(mockedAuthorize).toHaveBeenCalledWith( [{ permission: permissions.playlistListCreate }], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.createPlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -313,7 +307,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.getPlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -323,7 +317,7 @@ describe('createRouter', () => { const response = await request(app).get('/playlist-id').send(); expect(mockDbHandler.getPlaylist).toHaveBeenCalledWith( 'playlist-id', - mockUser, + mockCredentials.user().principal, ); expect(response.status).toEqual(200); expect(response.body).toEqual(mockPlaylist); @@ -346,7 +340,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.updatePlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -375,7 +369,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.deletePlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -404,7 +398,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.addPlaylistEntities).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -436,7 +430,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.getPlaylistEntities).not.toHaveBeenCalled(); expect(mockGetEntties).not.toHaveBeenCalled(); @@ -463,7 +457,12 @@ describe('createRouter', () => { }, ], }, - { token: 'token' }, + { + token: mockCredentials.service.token({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'catalog', + }), + }, ); expect(response.status).toEqual(200); expect(response.body).toEqual(sampleEntities); @@ -486,7 +485,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.removePlaylistEntities).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -518,7 +517,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.followPlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -528,7 +527,7 @@ describe('createRouter', () => { const response = await request(app).post('/playlist-id/followers').send(); expect(mockDbHandler.followPlaylist).toHaveBeenCalledWith( 'playlist-id', - mockUser, + mockCredentials.user().principal, ); expect(response.status).toEqual(200); }); @@ -550,7 +549,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.unfollowPlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -562,7 +561,7 @@ describe('createRouter', () => { .send(); expect(mockDbHandler.unfollowPlaylist).toHaveBeenCalledWith( 'playlist-id', - mockUser, + mockCredentials.user().principal, ); expect(response.status).toEqual(200); }); diff --git a/plugins/playlist-backend/src/service/router.ts b/plugins/playlist-backend/src/service/router.ts index 0397bff2ff..b73c7279f7 100644 --- a/plugins/playlist-backend/src/service/router.ts +++ b/plugins/playlist-backend/src/service/router.ts @@ -15,6 +15,7 @@ */ import { + createLegacyAuthAdapters, errorHandler, PluginDatabaseManager, PluginEndpointDiscovery, @@ -22,14 +23,10 @@ import { import { CatalogClient } from '@backstage/catalog-client'; import { parseEntityRef } from '@backstage/catalog-model'; import { NotAllowedError } from '@backstage/errors'; -import { - getBearerTokenFromAuthorizationHeader, - IdentityApi, -} from '@backstage/plugin-auth-node'; +import { IdentityApi } from '@backstage/plugin-auth-node'; import { AuthorizePermissionRequest, AuthorizeResult, - PermissionEvaluator, QueryPermissionRequest, } from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; @@ -44,6 +41,11 @@ import { Logger } from 'winston'; import { rules, transformConditions } from '../permissions'; import { DatabaseHandler } from './DatabaseHandler'; import { parseListPlaylistsFilterParams } from './ListPlaylistsFilter'; +import { + AuthService, + HttpAuthService, + PermissionsService, +} from '@backstage/backend-plugin-api'; /** * @public @@ -53,7 +55,9 @@ export interface RouterOptions { discovery: PluginEndpointDiscovery; identity: IdentityApi; logger: Logger; - permissions: PermissionEvaluator; + permissions: PermissionsService; + auth?: AuthService; + httpAuth?: HttpAuthService; } /** @@ -65,11 +69,12 @@ export async function createRouter( const { database, discovery, - identity, logger, permissions: permissionEvaluator, } = options; + const { auth, httpAuth } = createLegacyAuthAdapters(options); + logger.info('Initializing Playlist backend'); const catalogClient = new CatalogClient({ discoveryApi: discovery }); @@ -81,26 +86,21 @@ export async function createRouter( permission: AuthorizePermissionRequest | QueryPermissionRequest, conditional: boolean = false, ) => { - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); - - const user = await identity.getIdentity({ request }); - if (!user) { - throw new NotAllowedError('Unauthorized'); - } + const credentials = await httpAuth.credentials(request, { + allow: ['user'], + }); const decision = conditional ? ( await permissionEvaluator.authorizeConditional( [permission as QueryPermissionRequest], - { token }, + { credentials }, ) )[0] : ( await permissionEvaluator.authorize( [permission as AuthorizePermissionRequest], - { token }, + { credentials }, ) )[0]; @@ -108,7 +108,7 @@ export async function createRouter( throw new NotAllowedError('Unauthorized'); } - return { decision, user: user.identity }; + return { decision, user: credentials.principal }; }; const permissionIntegrationRouter = createPermissionIntegrationRouter({ @@ -227,11 +227,12 @@ export async function createRouter( }; }); - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }); - // TODO(kuanpg): entities in this playlist that no longer exist in the catalog will be + // TODO(kuangp): entities in this playlist that no longer exist in the catalog will be // excluded from this response, we need a way to clean up these orphaned refs potentially // via catalog events (https://github.com/backstage/backstage/issues/8219) // diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 30ce530b9b..5e4ed1e728 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -58,11 +58,11 @@ "express-promise-router": "^4.1.0", "http-proxy-middleware": "^2.0.0", "morgan": "^1.10.0", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yaml": "^2.0.0", "yn": "^4.0.0", - "yup": "^0.32.9" + "yup": "^1.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", @@ -70,7 +70,7 @@ "@backstage/config-loader": "workspace:^", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", - "@types/uuid": "^8.0.0", + "@types/uuid": "^9.0.0", "@types/yup": "^0.29.13", "msw": "^1.0.0", "supertest": "^6.1.3" diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index 26be04f108..b7902cc68a 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -47,7 +47,8 @@ "yaml": "^2.0.0" }, "devDependencies": { - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts index 27989650ce..e00362c457 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts @@ -18,11 +18,10 @@ import yaml from 'yaml'; import { ConfigReader } from '@backstage/config'; import { createPublishAzureAction } from './azure'; import { ScmIntegrations } from '@backstage/integration'; -import { getVoidLogger } from '@backstage/backend-common'; import { WebApi } from 'azure-devops-node-api'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { examples } from './azure.examples'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; jest.mock('azure-devops-node-api', () => ({ WebApi: jest.fn(), @@ -55,13 +54,7 @@ describe('publish:azure examples', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishAzureAction({ integrations, config }); - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); const mockGitClient = { createRepository: jest.fn(), diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts index 4401e01007..a23eebb93a 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts @@ -34,10 +34,9 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishAzureAction } from './azure'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { WebApi } from 'azure-devops-node-api'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:azure', () => { const config = new ConfigReader({ @@ -54,16 +53,10 @@ describe('publish:azure', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishAzureAction({ integrations, config }); - const mockContext = { - input: { - repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org', - }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + + const mockContext = createMockActionContext({ + input: { repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org' }, + }); const mockGitClient = { createRepository: jest.fn(), diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index 1a40b517d6..b6aa2acca4 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -50,6 +50,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts index 7045d58e01..6a079a3f9b 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts @@ -32,9 +32,8 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucketCloud', () => { const config = new ConfigReader({ @@ -50,17 +49,12 @@ describe('publish:bitbucketCloud', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketCloudAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', repoVisibility: 'private' as const, }, - workspacePath: 'wsp', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts index 90ae2c08b9..472c7a6a03 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts @@ -14,16 +14,15 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { PassThrough } from 'stream'; import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; import yaml from 'yaml'; import { examples } from './bitbucketCloudPipelinesRun.examples'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('bitbucket:pipelines:run', () => { const config = new ConfigReader({ @@ -39,14 +38,7 @@ describe('bitbucket:pipelines:run', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createBitbucketPipelinesRunAction({ integrations }); - const mockContext = { - input: {}, - workspacePath: 'wsp', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); const responseJson = { repository: { links: { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts index dc76a79898..9386246868 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts @@ -14,14 +14,13 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import { PassThrough } from 'stream'; import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('bitbucket:pipelines:run', () => { const config = new ConfigReader({ @@ -37,14 +36,7 @@ describe('bitbucket:pipelines:run', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createBitbucketPipelinesRunAction({ integrations }); - const mockContext = { - input: {}, - workspacePath: 'wsp', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); const workspace = 'test-workspace'; const repo_slug = 'test-repo-slug'; const responseJson = { diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index 8f4841180d..1f3abc2a8e 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -50,6 +50,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts index ab7b52996f..1d42e7368c 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts @@ -32,9 +32,8 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucketServer', () => { const config = new ConfigReader({ @@ -60,17 +59,12 @@ describe('publish:bitbucketServer', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketServerAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', repoVisibility: 'private' as const, }, - workspacePath: 'wsp', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts index 3af47853c3..9d2dd915f8 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts @@ -32,8 +32,7 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucketServer:pull-request', () => { const config = new ConfigReader({ @@ -62,7 +61,7 @@ describe('publish:bitbucketServer:pull-request', () => { integrations, config, }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', title: 'Add Scaffolder actions for Bitbucket Server', @@ -71,12 +70,7 @@ describe('publish:bitbucketServer:pull-request', () => { targetBranch: 'master', sourceBranch: 'develop', }, - workspacePath: 'wsp', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); const responseOfBranches = { size: 3, limit: 25, diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index c34c07c008..d05b85a9de 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -53,6 +53,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts index 73325ab5bf..c8f35fa0bf 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts @@ -32,12 +32,11 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; import { sep } from 'path'; import { examples } from './bitbucket.examples'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucket', () => { const config = new ConfigReader({ @@ -61,17 +60,12 @@ describe('publish:bitbucket', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', repoVisibility: 'private' as const, }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts index d6e068c039..a63d657904 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts @@ -31,9 +31,8 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucket', () => { const config = new ConfigReader({ @@ -57,17 +56,12 @@ describe('publish:bitbucket', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', repoVisibility: 'private' as const, }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index 07d72d73a6..d674cea8c4 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -54,6 +54,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts index f3bf1fef35..18ff0ad3ec 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PassThrough } from 'stream'; import { createConfluenceToMarkdownAction } from './confluenceToMarkdown'; import { getVoidLogger } from '@backstage/backend-common'; import { UrlReader } from '@backstage/backend-common'; @@ -28,6 +27,7 @@ import { setupServer } from 'msw/node'; import { examples } from './confluenceToMarkdown.examples'; import yaml from 'yaml'; import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('confluence:transform:markdown examples', () => { const baseUrl = `https://confluence.example.com`; @@ -71,14 +71,11 @@ describe('confluence:transform:markdown examples', () => { }), search: jest.fn(), }; - mockContext = { + mockContext = createMockActionContext({ input: yaml.parse(examples[0].example).steps[0].input, workspacePath, logger, - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); mockDir.setContent({ 'workspace/mkdocs.yml': 'File contents' }); }); diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts index 5ce8bb2697..eccb66f79e 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PassThrough } from 'stream'; + import { createConfluenceToMarkdownAction } from './confluenceToMarkdown'; import { getVoidLogger } from '@backstage/backend-common'; import { UrlReader } from '@backstage/backend-common'; @@ -26,6 +26,7 @@ import { import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('confluence:transform:markdown', () => { const baseUrl = `https://nodomain.confluence.com`; @@ -69,7 +70,7 @@ describe('confluence:transform:markdown', () => { }), search: jest.fn(), }; - mockContext = { + mockContext = createMockActionContext({ input: { confluenceUrls: [ 'https://nodomain.confluence.com/display/testing/mkdocs', @@ -79,10 +80,7 @@ describe('confluence:transform:markdown', () => { }, workspacePath, logger, - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); mockDir.setContent({ 'workspace/mkdocs.yml': 'File contents' }); }); @@ -221,7 +219,7 @@ describe('confluence:transform:markdown', () => { const action = createConfluenceToMarkdownAction(options); await expect(async () => { await action.handler(mockContext); - }).rejects.toThrow('Request failed with 401 Error'); + }).rejects.toThrow('Request failed with 401 nope'); }); it('should return nothing in results from the first api call and fail', async () => { @@ -284,6 +282,6 @@ describe('confluence:transform:markdown', () => { const action = createConfluenceToMarkdownAction(options); await expect(async () => { await action.handler(mockContext); - }).rejects.toThrow('Request failed with 404 Error'); + }).rejects.toThrow('Request failed with 404 nope'); }); }); diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 236ebd6c88..6d4223a565 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -53,6 +53,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^11.0.0" }, diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index af63ca0e48..92a636e58b 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -14,19 +14,15 @@ * limitations under the License. */ -import { - getVoidLogger, - UrlReader, - ContainerRunner, -} from '@backstage/backend-common'; +import { UrlReader, ContainerRunner } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; import { createMockDirectory } from '@backstage/backend-test-utils'; -import { PassThrough } from 'stream'; import { createFetchCookiecutterAction } from './cookiecutter'; import { join } from 'path'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; const executeShellCommand = jest.fn(); const commandExists = jest.fn(); @@ -88,7 +84,7 @@ describe('fetch:cookiecutter', () => { beforeEach(() => { jest.resetAllMocks(); - mockContext = { + mockContext = createMockActionContext({ input: { url: 'https://google.com/cookie/cutter', targetPath: 'something', @@ -101,11 +97,7 @@ describe('fetch:cookiecutter', () => { baseUrl: 'somebase', }, workspacePath: mockTmpDir, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), - }; + }); mockDir.setContent({ template: {} }); commandExists.mockResolvedValue(null); diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index dad19dc06b..be7abf15e1 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -49,6 +49,7 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts index 2bd7783cb6..c08544843f 100644 --- a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts @@ -33,9 +33,8 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:gerrit', () => { const config = new ConfigReader({ @@ -53,18 +52,13 @@ describe('publish:gerrit', () => { const description = 'for the lols'; const integrations = ScmIntegrations.fromConfig(config); const action = createPublishGerritAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gerrithost.org?owner=owner&workspace=parent&project=project&repo=repo', description, }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts index eeae3871ee..91236d4cc0 100644 --- a/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts @@ -24,9 +24,8 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishGerritReviewAction } from './gerritReview'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { commitAndPushRepo } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:gerrit:review', () => { const config = new ConfigReader({ @@ -43,18 +42,13 @@ describe('publish:gerrit:review', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishGerritReviewAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gerrithost.org?owner=owner&workspace=parent&project=project&repo=repo', gitCommitMessage: 'Review from backstage', }, - workspacePath: 'workspace', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index bc4912d76c..976f445ea5 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -49,6 +49,7 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 5e1e94b1b4..8675b1ff97 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PassThrough } from 'stream'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { createPublishGiteaAction } from './gitea'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { setupServer } from 'msw/node'; jest.mock('@backstage/plugin-scaffolder-node', () => { @@ -48,17 +47,12 @@ describe('publish:gitea', () => { const description = 'for the lols'; const integrations = ScmIntegrations.fromConfig(config); const action = createPublishGiteaAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitea.com?repo=repo&owner=owner', description, }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 34702c35b4..62b0415049 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -55,6 +55,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@types/libsodium-wrappers": "^0.7.10", "fs-extra": "^11.2.0", "jest-when": "^3.1.0", diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts index 7ea5e025ce..6a6c6daf13 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts @@ -36,14 +36,13 @@ import { TemplateAction, initRepoAndPush, } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { createPublishGithubAction } from './github'; import { examples } from './github.examples'; import yaml from 'yaml'; @@ -101,19 +100,14 @@ describe('publish:github', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', description: 'description', repoVisibility: 'private' as const, access: 'owner/blam', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { initRepoAndPushMocked.mockResolvedValue({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts index 75d9b74a8b..02d6a2afaf 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts @@ -34,15 +34,14 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { }); import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; import { when } from 'jest-when'; -import { PassThrough } from 'stream'; import { createPublishGithubAction } from './github'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { @@ -103,19 +102,14 @@ describe('publish:github', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', description: 'description', repoVisibility: 'private' as const, access: 'owner/blam', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { initRepoAndPushMocked.mockResolvedValue({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts index fe04142a98..1e59037c04 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts @@ -20,10 +20,9 @@ import { GithubCredentialsProvider, } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; import { createGithubActionsDispatchAction } from './githubActionsDispatch'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import yaml from 'yaml'; import { examples } from './githubActionsDispatch.examples'; @@ -56,18 +55,13 @@ describe('github:actions:dispatch', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', workflowId: 'a-workflow-id', branchOrTagName: 'main', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts index 3d59e43695..7f0d9b0490 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts @@ -20,9 +20,8 @@ import { GithubCredentialsProvider, } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createGithubActionsDispatchAction } from './githubActionsDispatch'; const mockOctokit = { @@ -54,18 +53,13 @@ describe('github:actions:dispatch', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', workflowId: 'a-workflow-id', branchOrTagName: 'main', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts index 612f68dea2..a686ea7f6d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, @@ -22,8 +21,8 @@ import { ScmIntegrations, } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; import { createGithubAutolinksAction } from './githubAutolinks'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { examples } from './githubAutolinks.examples'; import yaml from 'yaml'; @@ -55,9 +54,12 @@ describe('github:autolinks:create', () => { const integrations = ScmIntegrations.fromConfig(config); let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; + const input = yaml.parse(examples[0].example).steps[0].input; + const mockContext = createMockActionContext({ + input, + }); it('should call the githubApis for creating autolink reference', async () => { - const input = yaml.parse(examples[0].example).steps[0].input; githubCredentialsProvider = DefaultGithubCredentialsProvider.fromIntegrations(integrations); action = createGithubAutolinksAction({ @@ -70,14 +72,7 @@ describe('github:autolinks:create', () => { id: '1', }, }); - await action.handler({ - input, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }); + await action.handler(mockContext); expect(mockOctokit.rest.repos.createAutolink).toHaveBeenCalledWith({ owner: 'owner', diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts index 95c0226104..56115c156c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; import { createGithubAutolinksAction } from './githubAutolinks'; const mockOctokit = { @@ -53,13 +53,7 @@ describe('github:autolinks:create', () => { const integrations = ScmIntegrations.fromConfig(config); let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const workspacePath = createMockDirectory().resolve('workspace'); it('should call the githubApis for creating alphanumeric autolink reference', async () => { githubCredentialsProvider = @@ -74,14 +68,16 @@ describe('github:autolinks:create', () => { id: '1', }, }); - await action.handler({ - input: { - repoUrl: 'github.com?repo=repo&owner=owner', - keyPrefix: 'TICKET-', - urlTemplate: 'https://example.com/TICKET?query=', - }, - ...mockContext, - }); + await action.handler( + createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + keyPrefix: 'TICKET-', + urlTemplate: 'https://example.com/TICKET?query=', + }, + workspacePath, + }), + ); expect(mockOctokit.rest.repos.createAutolink).toHaveBeenCalledWith({ owner: 'owner', @@ -104,15 +100,17 @@ describe('github:autolinks:create', () => { id: '1', }, }); - await action.handler({ - input: { - repoUrl: 'github.com?repo=repo&owner=owner', - keyPrefix: 'TICKET-', - urlTemplate: 'https://example.com/TICKET?query=', - isAlphanumeric: false, - }, - ...mockContext, - }); + await action.handler( + createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + keyPrefix: 'TICKET-', + urlTemplate: 'https://example.com/TICKET?query=', + isAlphanumeric: false, + }, + workspacePath, + }), + ); expect(mockOctokit.rest.repos.createAutolink).toHaveBeenCalledWith({ owner: 'owner', diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts index c8e2a5f102..62e79dcd68 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts @@ -14,11 +14,10 @@ * limitations under the License. */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createGithubDeployKeyAction } from './githubDeployKey'; import yaml from 'yaml'; import { examples } from './githubDeployKey.examples'; -import { PassThrough } from 'stream'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -55,13 +54,7 @@ describe('Usage examples', () => { const integrations = ScmIntegrations.fromConfig(config); let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts index 4df9294ff5..798aa7705e 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts @@ -14,10 +14,9 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; import { createGithubDeployKeyAction } from './githubDeployKey'; -import { getVoidLogger } from '@backstage/backend-common'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -55,19 +54,14 @@ describe('github:deployKey:create', () => { const integrations = ScmIntegrations.fromConfig(config); let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repository&owner=owner', publicKey: 'pubkey', privateKey: 'privkey', deployKeyName: 'Push Tags', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts index 9d5dbfd35c..ecb4e9f092 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PassThrough } from 'stream'; import { createGithubEnvironmentAction } from './githubEnvironment'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -59,13 +58,7 @@ describe('github:environment:create examples', () => { const integrations = ScmIntegrations.fromConfig(config); let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { mockOctokit.rest.actions.getEnvironmentPublicKey.mockResolvedValue({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts index 8ec3d84e36..16872525b4 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; import { createGithubEnvironmentAction } from './githubEnvironment'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -58,17 +57,12 @@ describe('github:environment:create', () => { const integrations = ScmIntegrations.fromConfig(config); let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repository&owner=owner', name: 'envname', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { mockOctokit.rest.actions.getEnvironmentPublicKey.mockResolvedValue({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts index 9d74d53c6d..75ac6025ce 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts @@ -15,14 +15,13 @@ */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { createGithubIssuesLabelAction } from './githubIssuesLabel'; import yaml from 'yaml'; import { examples } from './githubIssuesLabel.examples'; @@ -64,13 +63,7 @@ describe('github:issues:label examples', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts index 3da16e677a..13c7df68cb 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts @@ -20,10 +20,9 @@ import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; import { getOctokitOptions } from './helpers'; jest.mock('./helpers', () => { @@ -62,18 +61,13 @@ describe('github:issues:label', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', number: '1', labels: ['label1', 'label2'], }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts index 7e5d107026..dd07c88a70 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts @@ -15,14 +15,13 @@ */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createPublishGithubPullRequestAction } from './githubPullRequest'; import yaml from 'yaml'; import { examples } from './githubPullRequest.examples'; @@ -57,13 +56,7 @@ describe('publish:github:pull-request examples', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); let fakeClient: { createPullRequest: jest.Mock; rest: { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts index 3fe4f62e04..c9d1498187 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createRootLogger, getRootLogger } from '@backstage/backend-common'; +import { createRootLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { GithubCredentialsProvider, @@ -25,9 +25,9 @@ import { TemplateAction, } from '@backstage/plugin-scaffolder-node'; import fs from 'fs-extra'; -import { Writable } from 'stream'; import { createPublishGithubPullRequestAction } from './githubPullRequest'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); @@ -131,14 +131,7 @@ describe('createPublishGithubPullRequestAction', () => { [workspacePath]: { 'file.txt': 'Hello there!' }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { @@ -196,14 +189,7 @@ describe('createPublishGithubPullRequestAction', () => { [workspacePath]: { 'file.txt': 'Hello there!' }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { @@ -263,14 +249,7 @@ describe('createPublishGithubPullRequestAction', () => { }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request with only relevant files', async () => { @@ -322,14 +301,7 @@ describe('createPublishGithubPullRequestAction', () => { [workspacePath]: { 'file.txt': 'Hello there!' }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { await instance.handler(ctx); @@ -382,14 +354,7 @@ describe('createPublishGithubPullRequestAction', () => { mockDir.setContent({ [workspacePath]: {} }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request and requests a review from the given reviewers', async () => { @@ -434,14 +399,7 @@ describe('createPublishGithubPullRequestAction', () => { mockDir.setContent({ [workspacePath]: {} }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('does not call the API endpoint for requesting reviewers', async () => { @@ -470,14 +428,7 @@ describe('createPublishGithubPullRequestAction', () => { }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { await instance.handler(ctx); @@ -526,14 +477,7 @@ describe('createPublishGithubPullRequestAction', () => { }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { await instance.handler(ctx); @@ -592,14 +536,7 @@ describe('createPublishGithubPullRequestAction', () => { }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { await instance.handler(ctx); @@ -653,14 +590,7 @@ describe('createPublishGithubPullRequestAction', () => { [workspacePath]: { 'file.txt': 'Hello there!' }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { @@ -705,14 +635,7 @@ describe('createPublishGithubPullRequestAction', () => { [workspacePath]: { 'file.txt': 'Hello there!' }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts index 2377e9a7eb..843a32f758 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts @@ -23,14 +23,13 @@ jest.mock('./gitHelpers', () => { }; }); -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createGithubRepoCreateAction } from './githubRepoCreate'; import { entityRefToName } from './gitHelpers'; import yaml from 'yaml'; @@ -82,16 +81,11 @@ describe('github:repo:create examples', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { githubCredentialsProvider = diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts index c72a969e96..25ddd176c1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts @@ -15,6 +15,7 @@ */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; jest.mock('./gitHelpers', () => { return { @@ -23,7 +24,6 @@ jest.mock('./gitHelpers', () => { }; }); -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, @@ -31,7 +31,6 @@ import { ScmIntegrations, } from '@backstage/integration'; import { when } from 'jest-when'; -import { PassThrough } from 'stream'; import { createGithubRepoCreateAction } from './githubRepoCreate'; import { entityRefToName } from './gitHelpers'; @@ -82,19 +81,14 @@ describe('github:repo:create', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', description: 'description', repoVisibility: 'private' as const, access: 'owner/blam', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { githubCredentialsProvider = diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts index 8f8c9bf8af..691cebd367 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts @@ -29,14 +29,13 @@ import { TemplateAction, initRepoAndPush, } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { createGithubRepoPushAction } from './githubRepoPush'; import { examples } from './githubRepoPush.examples'; import yaml from 'yaml'; @@ -102,13 +101,7 @@ describe('github:repo:push examples', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts index e16da0981d..206d4f067d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts @@ -59,14 +59,13 @@ import { TemplateAction, initRepoAndPush, } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { enableBranchProtectionOnDefaultRepoBranch } from './gitHelpers'; import { createGithubRepoPushAction } from './githubRepoPush'; @@ -103,19 +102,14 @@ describe('github:repo:push', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repository&owner=owner', description: 'description', repoVisibility: 'private' as const, access: 'owner/blam', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts index ce1fe76c53..d58d737f93 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts @@ -14,14 +14,13 @@ * limitations under the License. */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { createGithubWebhookAction } from './githubWebhook'; import yaml from 'yaml'; import { examples } from './githubWebhook.examples'; @@ -56,13 +55,7 @@ describe('github:webhook examples', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts index 7901210a6e..cbdcee6af1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts @@ -20,10 +20,9 @@ import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; const mockOctokit = { rest: { @@ -66,17 +65,12 @@ describe('github:repository:webhook:create', () => { }); }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', webhookUrl: 'https://example.com/payload', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); it('should call the githubApi for creating repository Webhook', async () => { const repoUrl = 'github.com?repo=repo&owner=owner'; diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index dc96a0f356..f24a967637 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -57,6 +57,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "jest-date-mock": "^1.0.8" }, "files": [ diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts index 72dcddc83d..4dc0305c2e 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; import { createGitlabGroupEnsureExistsAction } from './createGitlabGroupEnsureExistsAction'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; @@ -35,13 +34,7 @@ jest.mock('@gitbeaker/node', () => ({ })); describe('gitlab:group:ensureExists', () => { - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); afterEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts index fe9e556656..820ef8902e 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createGitlabIssueAction, IssueType } from './createGitlabIssueAction'; import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; @@ -60,18 +59,14 @@ describe('gitlab:issues:create', () => { const action = createGitlabIssueAction({ integrations }); it('should return a Gitlab issue when called with minimal input params', async () => { - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', projectId: 123, title: 'Computer banks to rule the world', }, workspacePath: 'seen2much', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); mockGitlabClient.Issues.create.mockResolvedValue({ id: 42, @@ -109,7 +104,7 @@ describe('gitlab:issues:create', () => { }); it('should return a Gitlab issue when called with oAuth Token', async () => { - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', projectId: 123, @@ -117,11 +112,7 @@ describe('gitlab:issues:create', () => { token: 'myAwesomeToken', }, workspacePath: 'seen2much', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); mockGitlabClient.Issues.create.mockResolvedValue({ id: 42, @@ -159,7 +150,7 @@ describe('gitlab:issues:create', () => { }); it('should return a Gitlab issue when called with several input params', async () => { - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', projectId: 123, @@ -173,11 +164,7 @@ describe('gitlab:issues:create', () => { labels: 'operation:mindcrime', }, workspacePath: 'seen2much', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); mockGitlabClient.Issues.create.mockResolvedValue({ id: 42, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts index ab4fe0e637..5afeaba8ae 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import { PassThrough } from 'stream'; import yaml from 'yaml'; import { createGitlabProjectAccessTokenAction } from './createGitlabProjectAccessTokenAction'; // Adjust the import based on your project structure import { examples } from './createGitlabProjectAccessTokenAction.examples'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { DateTime } from 'luxon'; @@ -59,16 +58,11 @@ describe('gitlab:projectAccessToken:create examples', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createGitlabProjectAccessTokenAction({ integrations }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts index 1d3ae804c1..cab72c9a6d 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts @@ -15,10 +15,9 @@ */ import { createGitlabProjectDeployTokenAction } from './createGitlabProjectDeployTokenAction'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; const mockGitlabClient = { ProjectDeployTokens: { @@ -52,7 +51,7 @@ describe('gitlab:create-deploy-token', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createGitlabProjectDeployTokenAction({ integrations }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', projectId: '123', @@ -60,12 +59,7 @@ describe('gitlab:create-deploy-token', () => { username: 'tokenuser', scopes: ['read_repository'], }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts index e5aa610ebc..4294e5f137 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import yaml from 'yaml'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; jest.mock('@backstage/plugin-scaffolder-node', () => { return { @@ -31,8 +32,6 @@ import { createPublishGitlabAction } from './gitlab'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { examples } from './gitlab.examples'; const mockGitlabClient = { @@ -76,16 +75,11 @@ describe('publish:gitlab', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishGitlabAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts index 5aca0a73e5..bbcae08063 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts @@ -29,9 +29,8 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishGitlabAction } from './gitlab'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; const mockGitlabClient = { Namespaces: { @@ -83,7 +82,8 @@ describe('publish:gitlab', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishGitlabAction({ integrations, config }); - const mockContext = { + + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', repoVisibility: 'private' as const, @@ -91,13 +91,8 @@ describe('publish:gitlab', () => { ci_config_path: '.gitlab-ci.yml', }, }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - const mockContextWithSettings = { + }); + const mockContextWithSettings = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', repoVisibility: 'private' as const, @@ -108,13 +103,8 @@ describe('publish:gitlab', () => { topics: ['topic1', 'topic2'], }, }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - const mockContextWithBranches = { + }); + const mockContextWithBranches = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', repoVisibility: 'private' as const, @@ -135,13 +125,8 @@ describe('publish:gitlab', () => { }, ], }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - const mockContextWithVariables = { + }); + const mockContextWithVariables = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', repoVisibility: 'private' as const, @@ -155,12 +140,7 @@ describe('publish:gitlab', () => { }, ], }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts index 71a2273aed..befc2c01c9 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createRootLogger, getRootLogger } from '@backstage/backend-common'; +import { createRootLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { Writable } from 'stream'; import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); @@ -78,6 +78,8 @@ describe('createGitLabMergeRequest', () => { const workspacePath = mockDir.resolve('workspace'); beforeEach(() => { + jest.clearAllMocks(); + mockDir.clear(); const config = new ConfigReader({ @@ -116,14 +118,7 @@ describe('createGitLabMergeRequest', () => { irrelevant: { 'bar.txt': 'Nothing to see here' }, }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Projects.show).not.toHaveBeenCalled(); @@ -158,14 +153,7 @@ describe('createGitLabMergeRequest', () => { irrelevant: { 'bar.txt': 'Nothing to see here' }, }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith('owner/repo'); @@ -203,14 +191,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -238,14 +219,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -279,14 +253,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -319,14 +286,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -359,14 +319,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -398,14 +351,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -433,14 +379,7 @@ describe('createGitLabMergeRequest', () => { irrelevant: { 'bar.txt': 'Nothing to see here' }, }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( @@ -482,14 +421,7 @@ describe('createGitLabMergeRequest', () => { irrelevant: { 'bar.txt': 'Nothing to see here' }, }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( @@ -526,14 +458,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( @@ -568,14 +493,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( @@ -610,14 +528,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( @@ -655,14 +566,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); @@ -700,14 +604,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); @@ -737,14 +634,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'create', }; - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await expect(instance.handler(ctx)).rejects.toThrow( 'Relative path is not allowed to refer to a directory outside its parent', diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts index 840f506540..569d3c0c56 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createRootLogger, getRootLogger } from '@backstage/backend-common'; +import { createRootLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { Writable } from 'stream'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { createGitlabRepoPushAction } from './gitlabRepoPush'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); @@ -93,14 +93,7 @@ describe('createGitLabCommit', () => { 'foo.txt': 'Hello there!', }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Branches.create).toHaveBeenCalledTimes(0); @@ -139,14 +132,7 @@ describe('createGitLabCommit', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Branches.create).toHaveBeenCalledTimes(0); @@ -183,14 +169,7 @@ describe('createGitLabCommit', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Branches.create).toHaveBeenCalledTimes(0); @@ -227,14 +206,7 @@ describe('createGitLabCommit', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Branches.create).toHaveBeenCalledTimes(0); @@ -276,14 +248,7 @@ describe('createGitLabCommit', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); @@ -325,14 +290,7 @@ describe('createGitLabCommit', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); @@ -366,14 +324,7 @@ describe('createGitLabCommit', () => { commitAction: 'create', }; - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await expect(instance.handler(ctx)).rejects.toThrow( 'Relative path is not allowed to refer to a directory outside its parent', @@ -398,14 +349,7 @@ describe('createGitLabCommit', () => { 'foo.txt': 'Hello there!', }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index f67f3fbb22..59950702c6 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -51,6 +51,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^11.0.0", "@types/node": "^18.17.8", diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index 7548ddd21d..b43ffcc95c 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -27,18 +27,14 @@ jest.mock('./railsNewRunner', () => { }; }); -import { - ContainerRunner, - getVoidLogger, - UrlReader, -} from '@backstage/backend-common'; +import { ContainerRunner, UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { resolve as resolvePath } from 'path'; -import { PassThrough } from 'stream'; import { createFetchRailsAction } from './index'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('fetch:rails', () => { const mockDir = createMockDirectory(); @@ -53,8 +49,7 @@ describe('fetch:rails', () => { }), ); - const mockTmpDir = mockDir.path; - const mockContext = { + const mockContext = createMockActionContext({ input: { url: 'https://rubyonrails.org/generator', targetPath: 'something', @@ -66,12 +61,8 @@ describe('fetch:rails', () => { baseUrl: 'somebase', entityRef: 'template:default/myTemplate', }, - workspacePath: mockTmpDir, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), - }; + workspacePath: mockDir.path, + }); const mockReader: UrlReader = { readUrl: jest.fn(), @@ -102,7 +93,7 @@ describe('fetch:rails', () => { expect(fetchContents).toHaveBeenCalledWith({ reader: mockReader, integrations, - baseUrl: mockContext.templateInfo.baseUrl, + baseUrl: mockContext.templateInfo?.baseUrl, fetchUrl: mockContext.input.url, outputPath: resolvePath(mockContext.workspacePath), }); @@ -112,7 +103,7 @@ describe('fetch:rails', () => { await action.handler(mockContext); expect(mockRailsTemplater.run).toHaveBeenCalledWith({ - workspacePath: mockTmpDir, + workspacePath: mockContext.workspacePath, logStream: mockContext.logStream, values: mockContext.input.values, }); @@ -128,7 +119,7 @@ describe('fetch:rails', () => { }); expect(mockRailsTemplater.run).toHaveBeenCalledWith({ - workspacePath: mockTmpDir, + workspacePath: mockContext.workspacePath, logStream: mockContext.logStream, values: { ...mockContext.input.values, diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index f1aab87354..3b1cfcc276 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -46,6 +46,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@backstage/types": "workspace:^", "msw": "^2.0.0" }, diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts index 972b0f1e8e..158b7db4ae 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts @@ -15,6 +15,7 @@ */ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { ActionContext } from '@backstage/plugin-scaffolder-node'; @@ -42,19 +43,17 @@ describe('sentry:project:create action', () => { name: string; slug?: string; authToken?: string; - }> => ({ - workspacePath: './dev/proj', - createTemporaryDirectory: jest.fn(), - logger: jest.createMockFromModule('winston'), - logStream: jest.createMockFromModule('stream'), - input: { - organizationSlug: 'org', - teamSlug: 'team', - name: 'test project', - authToken: randomBytes(5).toString('hex'), - }, - output: jest.fn(), - }); + }> => + createMockActionContext({ + workspacePath: './dev/proj', + logger: jest.createMockFromModule('winston'), + input: { + organizationSlug: 'org', + teamSlug: 'team', + name: 'test project', + authToken: randomBytes(5).toString('hex'), + }, + }); it('should request sentry project create with specified parameters.', async () => { expect.assertions(3); diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 2ea82dd5d2..6631d7833c 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -39,6 +39,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@backstage/types": "workspace:^", "winston": "^3.2.1", "yeoman-environment": "^3.9.1" diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts index 146c29f4ac..52bd95a3f9 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts @@ -18,9 +18,8 @@ import { yeomanRun } from './yeomanRun'; jest.mock('./yeomanRun'); -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import os from 'os'; -import { PassThrough } from 'stream'; import { createRunYeomanAction } from './yeoman'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { JsonObject } from '@backstage/types'; @@ -46,18 +45,14 @@ describe('run:yeoman', () => { const options = { code: 'owner', }; - mockContext = { + mockContext = createMockActionContext({ input: { namespace, args, options, }, workspacePath: mockTmpDir, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), - }; + }); await action.handler(mockContext); expect(yeomanRun).toHaveBeenCalledWith( diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 4df6d6c89f..61bd5ef49b 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -86,7 +86,7 @@ "p-limit": "^3.1.0", "p-queue": "^6.6.2", "prom-client": "^15.0.0", - "uuid": "^8.2.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yaml": "^2.0.0", "zen-observable": "^0.10.0", @@ -95,6 +95,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@types/fs-extra": "^11.0.0", "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts index 161ff9d1de..a368144784 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { createFetchCatalogEntityAction } from './fetch'; @@ -36,14 +34,9 @@ describe('catalog:fetch examples', () => { catalogClient: catalogClient as unknown as CatalogApi, }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), + const mockContext = createMockActionContext({ secrets: { backstageToken: 'secret' }, - }; + }); beforeEach(() => { jest.resetAllMocks(); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts index bed15f2e39..d3fd398a90 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { createFetchCatalogEntityAction } from './fetch'; @@ -34,14 +32,10 @@ describe('catalog:fetch', () => { catalogClient: catalogClient as unknown as CatalogApi, }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), + const mockContext = createMockActionContext({ secrets: { backstageToken: 'secret' }, - }; + }); + beforeEach(() => { jest.resetAllMocks(); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts index eb5aa88c0f..db3d7151d8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -44,13 +42,7 @@ describe('catalog:register', () => { catalogClient: catalogClient as unknown as CatalogApi, }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts index bae9f04575..8f6219b64c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -42,13 +40,8 @@ describe('catalog:register', () => { catalogClient: catalogClient as unknown as CatalogApi, }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); + beforeEach(() => { jest.resetAllMocks(); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts index daa8f4f6b1..e6b4bcde47 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts @@ -20,24 +20,16 @@ jest.mock('fs-extra'); const fsMock = fs as jest.Mocked; -import { PassThrough } from 'stream'; -import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createCatalogWriteAction } from './write'; import { resolve as resolvePath } from 'path'; import * as yaml from 'yaml'; import { examples } from './write.examples'; +import os from 'os'; describe('catalog:write', () => { const action = createCatalogWriteAction(); - - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext({ workspacePath: os.tmpdir() }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts index dd6f29f99b..cc22b75da2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts @@ -20,9 +20,8 @@ jest.mock('fs-extra'); const fsMock = fs as jest.Mocked; -import { PassThrough } from 'stream'; import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model'; import { createCatalogWriteAction } from './write'; import { resolve as resolvePath } from 'path'; @@ -31,18 +30,14 @@ import * as yaml from 'yaml'; describe('catalog:write', () => { const action = createCatalogWriteAction(); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - beforeEach(() => { jest.resetAllMocks(); }); + const mockContext = createMockActionContext({ + workspacePath: os.tmpdir(), + }); + it('should write the catalog-info.yml in the workspace', async () => { const entity = { apiVersion: 'backstage.io/v1alpha1', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts index 901f3e5011..06addba98d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; @@ -30,15 +30,10 @@ describe('debug:log examples', () => { const mockDir = createMockDirectory(); const workspacePath = mockDir.resolve('workspace'); - const mockContext = { - input: {}, - baseUrl: 'somebase', - workspacePath, - logger: getVoidLogger(), + const mockContext = createMockActionContext({ logStream, - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + workspacePath, + }); const action = createDebugLogAction(); @@ -51,12 +46,10 @@ describe('debug:log examples', () => { }); it('should log message', async () => { - const context = { + await action.handler({ ...mockContext, input: yaml.parse(examples[0].example).steps[0].input, - }; - - await action.handler(context); + }); expect(logStream.write).toHaveBeenCalledTimes(1); expect(logStream.write).toHaveBeenCalledWith( @@ -65,12 +58,10 @@ describe('debug:log examples', () => { }); it('should log the workspace content, if active', async () => { - const context = { + await action.handler({ ...mockContext, input: yaml.parse(examples[1].example).steps[0].input, - }; - - await action.handler(context); + }); expect(logStream.write).toHaveBeenCalledTimes(1); expect(logStream.write).toHaveBeenCalledWith( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts index d6fc5174b3..09c090582a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; @@ -29,15 +29,7 @@ describe('debug:log', () => { const mockDir = createMockDirectory(); const workspacePath = mockDir.resolve('workspace'); - const mockContext = { - input: {}, - baseUrl: 'somebase', - workspacePath, - logger: getVoidLogger(), - logStream, - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext({ workspacePath, logStream }); const action = createDebugLogAction(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts index 9b755d86dd..8bc29a74c9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { createWaitAction } from './wait'; import { Writable } from 'stream'; import { examples } from './wait.examples'; import yaml from 'yaml'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('debug:wait examples', () => { const action = createWaitAction(); @@ -28,18 +27,9 @@ describe('debug:wait examples', () => { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockDir = createMockDirectory(); - const workspacePath = mockDir.resolve('workspace'); - - const mockContext = { - input: {}, - baseUrl: 'somebase', - workspacePath, - logger: getVoidLogger(), + const mockContext = createMockActionContext({ logStream, - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts index 6f80604a6d..0dcbd10f0f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts @@ -14,10 +14,9 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { createWaitAction } from './wait'; import { Writable } from 'stream'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('debug:wait', () => { const action = createWaitAction(); @@ -26,18 +25,9 @@ describe('debug:wait', () => { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockDir = createMockDirectory(); - const workspacePath = mockDir.resolve('workspace'); - - const mockContext = { - input: {}, - baseUrl: 'somebase', - workspacePath, - logger: getVoidLogger(), + const mockContext = createMockActionContext({ logStream, - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts index 6b308a97eb..82a32fcbac 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts @@ -16,14 +16,13 @@ import yaml from 'yaml'; -import os from 'os'; import { resolve as resolvePath } from 'path'; -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { createFetchPlainAction } from './plain'; -import { PassThrough } from 'stream'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { examples } from './plain.examples'; jest.mock('@backstage/plugin-scaffolder-node', () => ({ @@ -50,19 +49,15 @@ describe('fetch:plain examples', () => { }); const action = createFetchPlainAction({ integrations, reader }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); it('should fetch plain', async () => { - await action.handler({ - ...mockContext, - input: yaml.parse(examples[0].example).steps[0].input, - }); + await action.handler( + createMockActionContext({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }), + ); expect(fetchContents).toHaveBeenCalledWith( expect.objectContaining({ outputPath: resolvePath(mockContext.workspacePath), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts index 917624acf4..bc20fafe19 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts @@ -19,14 +19,13 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { return { ...actual, fetchContents: jest.fn() }; }); -import os from 'os'; import { resolve as resolvePath } from 'path'; -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; import { createFetchPlainAction } from './plain'; -import { PassThrough } from 'stream'; describe('fetch:plain', () => { const integrations = ScmIntegrations.fromConfig( @@ -47,13 +46,7 @@ describe('fetch:plain', () => { }); const action = createFetchPlainAction({ integrations, reader }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); it('should disallow a target path outside working directory', async () => { await expect( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts index 2ee17c29fe..9978d6d8f5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts @@ -14,19 +14,19 @@ * limitations under the License. */ +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; + jest.mock('@backstage/plugin-scaffolder-node', () => { const actual = jest.requireActual('@backstage/plugin-scaffolder-node'); return { ...actual, fetchFile: jest.fn() }; }); import yaml from 'yaml'; -import os from 'os'; import { resolve as resolvePath } from 'path'; -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { createFetchPlainFileAction } from './plainFile'; -import { PassThrough } from 'stream'; import { fetchFile } from '@backstage/plugin-scaffolder-node'; import { examples } from './plainFile.examples'; @@ -49,13 +49,7 @@ describe('fetch:plain:file examples', () => { }); const action = createFetchPlainFileAction({ integrations, reader }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); it('should fetch plain', async () => { await action.handler({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts index 7ea74a809b..bf40c7ad0e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts @@ -19,14 +19,13 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { return { ...actual, fetchFile: jest.fn() }; }); -import os from 'os'; import { resolve as resolvePath } from 'path'; -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { fetchFile } from '@backstage/plugin-scaffolder-node'; import { createFetchPlainFileAction } from './plainFile'; -import { PassThrough } from 'stream'; describe('fetch:plain:file', () => { const integrations = ScmIntegrations.fromConfig( @@ -47,13 +46,7 @@ describe('fetch:plain:file', () => { }); const action = createFetchPlainFileAction({ integrations, reader }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); it('should disallow a target path outside working directory', async () => { await expect( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts index a1c8381178..36f1f00b50 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts @@ -16,13 +16,9 @@ import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; -import { - getVoidLogger, - resolvePackagePath, - UrlReader, -} from '@backstage/backend-common'; +import { resolvePackagePath, UrlReader } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createFetchTemplateAction } from './template'; import { ActionContext, @@ -61,23 +57,15 @@ describe('fetch:template examples', () => { const mockDir = createMockDirectory(); const workspacePath = mockDir.resolve('workspace'); - const logger = getVoidLogger(); - - const mockContext = (input: any) => ({ - templateInfo: { - baseUrl: 'base-url', - entityRef: 'template:default/test-template', - }, - input: input, - output: jest.fn(), - logStream: new PassThrough(), - logger, - workspacePath, - - async createTemporaryDirectory() { - return fs.mkdtemp(mockDir.resolve('tmp-')); - }, - }); + const mockContext = (input: any) => + createMockActionContext({ + templateInfo: { + baseUrl: 'base-url', + entityRef: 'template:default/test-template', + }, + input, + workspacePath, + }); beforeEach(() => { mockDir.clear(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 25b65462e6..fd0ef7d757 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -21,13 +21,8 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; -import { - getVoidLogger, - resolvePackagePath, - UrlReader, -} from '@backstage/backend-common'; +import { resolvePackagePath, UrlReader } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { createFetchTemplateAction } from './template'; import { fetchContents, @@ -35,6 +30,7 @@ import { TemplateAction, } from '@backstage/plugin-scaffolder-node'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; type FetchTemplateInput = ReturnType< typeof createFetchTemplateAction @@ -59,29 +55,22 @@ describe('fetch:template', () => { const mockDir = createMockDirectory(); const workspacePath = mockDir.resolve('workspace'); - const logger = getVoidLogger(); - - const mockContext = (inputPatch: Partial = {}) => ({ - templateInfo: { - baseUrl: 'base-url', - entityRef: 'template:default/test-template', - }, - input: { - url: './skeleton', - targetPath: './target', - values: { - test: 'value', + const mockContext = (inputPatch: Partial = {}) => + createMockActionContext({ + templateInfo: { + baseUrl: 'base-url', + entityRef: 'template:default/test-template', }, - ...inputPatch, - }, - output: jest.fn(), - logStream: new PassThrough(), - logger, - workspacePath, - async createTemporaryDirectory() { - return fs.mkdtemp(mockDir.resolve('tmp-')); - }, - }); + input: { + url: './skeleton', + targetPath: './target', + values: { + test: 'value', + }, + ...inputPatch, + }, + workspacePath, + }); beforeEach(() => { mockDir.setContent({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts index 023f049ebc..d11b1e4087 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts @@ -15,8 +15,7 @@ */ import { createFilesystemDeleteAction } from './delete'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { resolve as resolvePath } from 'path'; import fs from 'fs-extra'; import yaml from 'yaml'; @@ -31,16 +30,12 @@ describe('fs:delete examples', () => { const files: string[] = yaml.parse(examples[0].example).steps[0].input.files; - const mockContext = { + const mockContext = createMockActionContext({ input: { files: files, }, workspacePath, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.restoreAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts index 2a4f45b862..8226133376 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts @@ -16,8 +16,7 @@ import { resolve as resolvePath } from 'path'; import { createFilesystemDeleteAction } from './delete'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import fs from 'fs-extra'; import { createMockDirectory } from '@backstage/backend-test-utils'; @@ -27,16 +26,12 @@ describe('fs:delete', () => { const mockDir = createMockDirectory(); const workspacePath = resolvePath(mockDir.path, 'workspace'); - const mockContext = { + const mockContext = createMockActionContext({ input: { files: ['unit-test-a.js', 'unit-test-b.js'], }, workspacePath, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.restoreAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts index cbb0fa0d0b..0881bb8260 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts @@ -16,8 +16,7 @@ import { resolve as resolvePath } from 'path'; import { createFilesystemRenameAction } from './rename'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import fs from 'fs-extra'; import yaml from 'yaml'; import { examples } from './rename.examples'; @@ -31,16 +30,12 @@ describe('fs:rename examples', () => { const mockDir = createMockDirectory(); const workspacePath = resolvePath(mockDir.path, 'workspace'); - const mockContext = { + const mockContext = createMockActionContext({ input: { - files: files, + files, }, workspacePath, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.restoreAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts index d37e967f43..6696fda693 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts @@ -16,8 +16,7 @@ import { resolve as resolvePath } from 'path'; import { createFilesystemRenameAction } from './rename'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import fs from 'fs-extra'; import { createMockDirectory } from '@backstage/backend-test-utils'; @@ -41,16 +40,12 @@ describe('fs:rename', () => { to: 'brand-new-folder', }, ]; - const mockContext = { + const mockContext = createMockActionContext({ input: { files: mockInputFiles, }, workspacePath, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.restoreAllMocks(); diff --git a/plugins/scaffolder-node-test-utils/.eslintrc.js b/plugins/scaffolder-node-test-utils/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/scaffolder-node-test-utils/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-node-test-utils/CHANGELOG.md b/plugins/scaffolder-node-test-utils/CHANGELOG.md new file mode 100644 index 0000000000..2943a2a755 --- /dev/null +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -0,0 +1 @@ +# @backstage/plugin-scaffolder-node-test-utils diff --git a/plugins/scaffolder-node-test-utils/README.md b/plugins/scaffolder-node-test-utils/README.md new file mode 100644 index 0000000000..851ddf808d --- /dev/null +++ b/plugins/scaffolder-node-test-utils/README.md @@ -0,0 +1,12 @@ +# @backstage/plugin-scaffolder-node-test-utils + +Contains utilities that can be used when testing scaffolder features. + +## Installation + +Install the package via Yarn into your own packages: + +```sh +cd # if within a monorepo +yarn add --dev @backstage/plugin-scaffolder-node-test-utils +``` diff --git a/plugins/scaffolder-node-test-utils/api-report.md b/plugins/scaffolder-node-test-utils/api-report.md new file mode 100644 index 0000000000..7d508c3c38 --- /dev/null +++ b/plugins/scaffolder-node-test-utils/api-report.md @@ -0,0 +1,33 @@ +## API Report File for "@backstage/plugin-scaffolder-node-test-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { JsonObject } from '@backstage/types'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; +import * as winston from 'winston'; +import { Writable } from 'stream'; + +// @public +export const createMockActionContext: < + TActionInput extends JsonObject = JsonObject, + TActionOutput extends JsonObject = JsonObject, +>( + options?: + | { + input?: TActionInput | undefined; + logger?: winston.Logger | undefined; + logStream?: Writable | undefined; + secrets?: TaskSecrets | undefined; + templateInfo?: TemplateInfo | undefined; + workspacePath?: string | undefined; + } + | undefined, +) => ActionContext; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/scaffolder-node-test-utils/catalog-info.yaml b/plugins/scaffolder-node-test-utils/catalog-info.yaml new file mode 100644 index 0000000000..980bd07e34 --- /dev/null +++ b/plugins/scaffolder-node-test-utils/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-node-test-utils + title: '@backstage/plugin-scaffolder-node-test-utils' +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/plugins/scaffolder-node-test-utils/knip-report.md b/plugins/scaffolder-node-test-utils/knip-report.md new file mode 100644 index 0000000000..2661c35327 --- /dev/null +++ b/plugins/scaffolder-node-test-utils/knip-report.md @@ -0,0 +1,2 @@ +# Knip report + diff --git a/plugins/scaffolder-node-test-utils/package.json b/plugins/scaffolder-node-test-utils/package.json new file mode 100644 index 0000000000..8c30261707 --- /dev/null +++ b/plugins/scaffolder-node-test-utils/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-scaffolder-node-test-utils", + "version": "0.0.1", + "backstage": { + "role": "node-library" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/scaffolder-node-test-utils" + }, + "license": "Apache-2.0", + "sideEffects": false, + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-common": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", + "@backstage/types": "workspace:^", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@types/react": "*" + } +} diff --git a/plugins/scaffolder-node-test-utils/src/actions/index.ts b/plugins/scaffolder-node-test-utils/src/actions/index.ts new file mode 100644 index 0000000000..161bae8521 --- /dev/null +++ b/plugins/scaffolder-node-test-utils/src/actions/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createMockActionContext } from './mockActionConext'; diff --git a/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts b/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts new file mode 100644 index 0000000000..1b9f6200f9 --- /dev/null +++ b/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough, Writable } from 'stream'; +import { getVoidLogger } from '@backstage/backend-common'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { JsonObject } from '@backstage/types'; +import { ActionContext, TaskSecrets } from '@backstage/plugin-scaffolder-node'; +import * as winston from 'winston'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; + +/** + * A utility method to create a mock action context for scaffolder actions. + * + * @public + * @param options - optional parameters to override default mock context + */ +export const createMockActionContext = < + TActionInput extends JsonObject = JsonObject, + TActionOutput extends JsonObject = JsonObject, +>(options?: { + input?: TActionInput; + logger?: winston.Logger; + logStream?: Writable; + secrets?: TaskSecrets; + templateInfo?: TemplateInfo; + workspacePath?: string; +}): ActionContext => { + const defaultContext = { + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + input: {} as TActionInput, + }; + + const createDefaultWorkspace = () => ({ + workspacePath: createMockDirectory().resolve('workspace'), + }); + + if (!options) { + return { + ...defaultContext, + ...createDefaultWorkspace(), + }; + } + + const { input, logger, logStream, secrets, templateInfo, workspacePath } = + options; + + return { + ...defaultContext, + ...(workspacePath ? { workspacePath } : createDefaultWorkspace()), + ...(workspacePath && { + createTemporaryDirectory: jest.fn().mockResolvedValue(workspacePath), + }), + ...(logger && { logger }), + ...(logStream && { logStream }), + ...(input && { input }), + ...(secrets && { secrets }), + templateInfo, + }; +}; diff --git a/plugins/scaffolder-node-test-utils/src/index.ts b/plugins/scaffolder-node-test-utils/src/index.ts new file mode 100644 index 0000000000..19eae8d569 --- /dev/null +++ b/plugins/scaffolder-node-test-utils/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './actions'; diff --git a/plugins/scaffolder-react/api-report-alpha.md b/plugins/scaffolder-react/api-report-alpha.md index a521fecc70..683b8cf8be 100644 --- a/plugins/scaffolder-react/api-report-alpha.md +++ b/plugins/scaffolder-react/api-report-alpha.md @@ -16,6 +16,7 @@ import { IconComponent } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; +import { Overrides } from '@material-ui/core/styles/overrides'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; @@ -25,6 +26,7 @@ import { ScaffolderRJSFFormProps } from '@backstage/plugin-scaffolder-react'; import { ScaffolderStep } from '@backstage/plugin-scaffolder-react'; import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; import { SetStateAction } from 'react'; +import { StyleRules } from '@material-ui/core/styles/withStyles'; import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react'; @@ -32,6 +34,13 @@ import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react'; import { TemplatePresentationV1beta3 } from '@backstage/plugin-scaffolder-common'; import { UiSchema } from '@rjsf/utils'; +// @alpha (undocumented) +export type BackstageOverrides = Overrides & { + [Name in keyof ScaffolderReactComponentsNameToClassKey]?: Partial< + StyleRules + >; +}; + // @alpha (undocumented) export const createAsyncValidators: ( rootSchema: JsonObject, @@ -132,6 +141,14 @@ export type ScaffolderPageContextMenuProps = { onCreateClicked?: () => void; }; +// @alpha (undocumented) +export type ScaffolderReactComponentsNameToClassKey = { + ScaffolderReactTemplateCategoryPicker: ScaffolderReactTemplateCategoryPickerClassKey; +}; + +// @alpha (undocumented) +export type ScaffolderReactTemplateCategoryPickerClassKey = 'root' | 'label'; + // @alpha export const Stepper: (stepperProps: StepperProps) => React_2.JSX.Element; diff --git a/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx index eba6aebf93..9c85cacfc2 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx @@ -23,6 +23,7 @@ import { FormControlLabel, TextField, Typography, + makeStyles, } from '@material-ui/core'; import CheckBoxIcon from '@material-ui/icons/CheckBox'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; @@ -34,12 +35,24 @@ import { alertApiRef, useApi } from '@backstage/core-plugin-api'; const icon = ; const checkedIcon = ; +/** @alpha */ +export type ScaffolderReactTemplateCategoryPickerClassKey = 'root' | 'label'; + +const useStyles = makeStyles( + { + root: {}, + label: {}, + }, + { name: 'ScaffolderReactTemplateCategoryPicker' }, +); + /** * The Category Picker that is rendered on the left side for picking * categories and filtering the template list. * @alpha */ export const TemplateCategoryPicker = () => { + const classes = useStyles(); const alertApi = useApi(alertApiRef); const { error, loading, availableTypes, selectedTypes, setSelectedTypes } = useEntityTypeFilter(); @@ -57,8 +70,9 @@ export const TemplateCategoryPicker = () => { if (!availableTypes) return null; return ( - + + >; +}; + +declare module '@backstage/theme' { + interface OverrideComponentNameToClassKeys + extends ScaffolderReactComponentsNameToClassKey {} +} diff --git a/plugins/search-backend-module-catalog/api-report.md b/plugins/search-backend-module-catalog/api-report.md index 6f5813ca9e..fc19ede2d4 100644 --- a/plugins/search-backend-module-catalog/api-report.md +++ b/plugins/search-backend-module-catalog/api-report.md @@ -5,6 +5,7 @@ ```ts /// +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; import { Config } from '@backstage/config'; @@ -41,6 +42,7 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { // @public (undocumented) export type DefaultCatalogCollatorFactoryOptions = { + auth?: AuthService; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; locationTemplate?: string; diff --git a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts index 586346c385..166985f2d5 100644 --- a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts +++ b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts @@ -17,6 +17,7 @@ import { PluginEndpointDiscovery, TokenManager, + createLegacyAuthAdapters, } from '@backstage/backend-common'; import { CatalogApi, @@ -33,9 +34,11 @@ import { Readable } from 'stream'; import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; import { readCollatorConfigOptions } from './config'; import { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer'; +import { AuthService } from '@backstage/backend-plugin-api'; /** @public */ export type DefaultCatalogCollatorFactoryOptions = { + auth?: AuthService; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; /** @@ -71,20 +74,26 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { private filter?: GetEntitiesRequest['filter']; private batchSize: number; private readonly catalogClient: CatalogApi; - private tokenManager: TokenManager; private entityTransformer: CatalogCollatorEntityTransformer; + private auth: AuthService; static fromConfig( configRoot: Config, options: DefaultCatalogCollatorFactoryOptions, ) { const configOptions = readCollatorConfigOptions(configRoot); + const { auth: adaptedAuth } = createLegacyAuthAdapters({ + auth: options.auth, + discovery: options.discovery, + tokenManager: options.tokenManager, + }); return new DefaultCatalogCollatorFactory({ locationTemplate: options.locationTemplate ?? configOptions.locationTemplate, filter: options.filter ?? configOptions.filter, batchSize: options.batchSize ?? configOptions.batchSize, entityTransformer: options.entityTransformer, + auth: adaptedAuth, discovery: options.discovery, tokenManager: options.tokenManager, catalogClient: options.catalogClient, @@ -96,17 +105,18 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { filter: GetEntitiesRequest['filter']; batchSize: number; entityTransformer?: CatalogCollatorEntityTransformer; + auth: AuthService; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; catalogClient?: CatalogApi; }) { const { + auth, batchSize, discovery, locationTemplate, filter, catalogClient, - tokenManager, entityTransformer, } = options; @@ -115,9 +125,9 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { this.batchSize = batchSize; this.catalogClient = catalogClient || new CatalogClient({ discoveryApi: discovery }); - this.tokenManager = tokenManager; this.entityTransformer = entityTransformer ?? defaultCatalogCollatorEntityTransformer; + this.auth = auth; } async getCollator(): Promise { @@ -125,7 +135,6 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { } private async *execute(): AsyncGenerator { - const { token } = await this.tokenManager.getToken(); let entitiesRetrieved = 0; let moreEntitiesToGet = true; @@ -133,6 +142,10 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { // limit (and allow some control over) memory used by the search backend // at index-time. while (moreEntitiesToGet) { + const { token } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); const entities = ( await this.catalogClient.getEntities( { diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 88b6d9542a..f177121a2e 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -57,7 +57,7 @@ "aws4": "^1.12.0", "elastic-builder": "^2.16.0", "lodash": "^4.17.21", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/search-backend-module-explore/api-report.md b/plugins/search-backend-module-explore/api-report.md index 50cda184a9..43788340c0 100644 --- a/plugins/search-backend-module-explore/api-report.md +++ b/plugins/search-backend-module-explore/api-report.md @@ -5,6 +5,7 @@ ```ts /// +import { AuthService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { ExploreTool } from '@backstage/plugin-explore-common'; @@ -37,5 +38,6 @@ export type ToolDocumentCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; logger: Logger; tokenManager?: TokenManager; + auth?: AuthService; }; ``` diff --git a/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.ts b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.ts index bf6f07839b..daf83ec02b 100644 --- a/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.ts +++ b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.ts @@ -17,7 +17,9 @@ import { PluginEndpointDiscovery, TokenManager, + createLegacyAuthAdapters, } from '@backstage/backend-common'; +import { AuthService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { ExploreTool } from '@backstage/plugin-explore-common'; import { @@ -44,6 +46,7 @@ export type ToolDocumentCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; logger: Logger; tokenManager?: TokenManager; + auth?: AuthService; }; /** @@ -56,12 +59,13 @@ export class ToolDocumentCollatorFactory implements DocumentCollatorFactory { private readonly discovery: PluginEndpointDiscovery; private readonly logger: Logger; - private readonly tokenManager?: TokenManager; + private readonly auth: AuthService; private constructor(options: ToolDocumentCollatorFactoryOptions) { this.discovery = options.discovery; this.logger = options.logger; - this.tokenManager = options.tokenManager; + + this.auth = createLegacyAuthAdapters(options).auth; } static fromConfig( @@ -94,16 +98,13 @@ export class ToolDocumentCollatorFactory implements DocumentCollatorFactory { private async fetchTools() { const baseUrl = await this.discovery.getBaseUrl('explore'); - let headers = {}; - - if (this.tokenManager) { - const { token } = await this.tokenManager.getToken(); - headers = { - Authorization: `Bearer ${token}`, - }; - } - - const response = await fetch(`${baseUrl}/tools`, headers); + const { token } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'explore', + }); + const response = await fetch(`${baseUrl}/tools`, { + headers: { Authorization: `Bearer ${token}` }, + }); if (!response.ok) { throw new Error( diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 2ba5331a4d..9344e1c5ba 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -54,7 +54,7 @@ "@backstage/plugin-search-common": "workspace:^", "knex": "^3.0.0", "lodash": "^4.17.21", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/search-backend-module-techdocs/api-report.md b/plugins/search-backend-module-techdocs/api-report.md index 592cf5065a..1a8c92e5bc 100644 --- a/plugins/search-backend-module-techdocs/api-report.md +++ b/plugins/search-backend-module-techdocs/api-report.md @@ -5,10 +5,12 @@ ```ts /// +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { Entity } from '@backstage/catalog-model'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -44,6 +46,8 @@ export type TechDocsCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; logger: Logger; tokenManager: TokenManager; + auth?: AuthService; + httpAuth?: HttpAuthService; locationTemplate?: string; catalogClient?: CatalogApi; parallelismLimit?: number; diff --git a/plugins/search-backend-module-techdocs/src/alpha.ts b/plugins/search-backend-module-techdocs/src/alpha.ts index e8be7cc864..f7a6bb0d36 100644 --- a/plugins/search-backend-module-techdocs/src/alpha.ts +++ b/plugins/search-backend-module-techdocs/src/alpha.ts @@ -74,6 +74,8 @@ export default createBackendModule({ deps: { config: coreServices.rootConfig, logger: coreServices.logger, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, discovery: coreServices.discovery, tokenManager: coreServices.tokenManager, scheduler: coreServices.scheduler, @@ -83,6 +85,8 @@ export default createBackendModule({ async init({ config, logger, + auth, + httpAuth, discovery, tokenManager, scheduler, @@ -106,6 +110,8 @@ export default createBackendModule({ factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, tokenManager, + auth, + httpAuth, logger: loggerToWinstonLogger(logger), catalogClient: catalog, entityTransformer: transformer, diff --git a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts index b76fedbdf9..1234c5c6fa 100644 --- a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts +++ b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts @@ -17,6 +17,7 @@ import { PluginEndpointDiscovery, TokenManager, + createLegacyAuthAdapters, } from '@backstage/backend-common'; import { CatalogApi, @@ -41,6 +42,7 @@ import { Readable } from 'stream'; import { Logger } from 'winston'; import { TechDocsCollatorEntityTransformer } from './TechDocsCollatorEntityTransformer'; import { defaultTechDocsCollatorEntityTransformer } from './defaultTechDocsCollatorEntityTransformer'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; interface MkSearchIndexDoc { title: string; @@ -57,6 +59,8 @@ export type TechDocsCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; logger: Logger; tokenManager: TokenManager; + auth?: AuthService; + httpAuth?: HttpAuthService; locationTemplate?: string; catalogClient?: CatalogApi; parallelismLimit?: number; @@ -84,8 +88,8 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { private discovery: PluginEndpointDiscovery; private locationTemplate: string; private readonly logger: Logger; + private readonly auth: AuthService; private readonly catalogClient: CatalogApi; - private readonly tokenManager: TokenManager; private readonly parallelismLimit: number; private readonly legacyPathCasing: boolean; private entityTransformer: TechDocsCollatorEntityTransformer; @@ -100,9 +104,14 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { new CatalogClient({ discoveryApi: options.discovery }); this.parallelismLimit = options.parallelismLimit ?? 10; this.legacyPathCasing = options.legacyPathCasing ?? false; - this.tokenManager = options.tokenManager; this.entityTransformer = options.entityTransformer ?? defaultTechDocsCollatorEntityTransformer; + + this.auth = createLegacyAuthAdapters({ + auth: options.auth, + discovery: options.discovery, + tokenManager: options.tokenManager, + }).auth; } static fromConfig(config: Config, options: TechDocsCollatorFactoryOptions) { @@ -131,7 +140,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { private async *execute(): AsyncGenerator { const limit = pLimit(this.parallelismLimit); const techDocsBaseUrl = await this.discovery.getBaseUrl('techdocs'); - const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; let moreEntitiesToGet = true; @@ -141,6 +150,11 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { // parallelism limit to simplify configuration. const batchSize = this.parallelismLimit * 50; while (moreEntitiesToGet) { + const { token: catalogToken } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); + const entities = ( await this.catalogClient.getEntities( { @@ -151,7 +165,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { limit: batchSize, offset: entitiesRetrieved, }, - { token }, + { token: catalogToken }, ) ).items; @@ -174,6 +188,12 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { ); try { + const { token: techdocsToken } = + await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'techdocs', + }); + const searchIndexResponse = await fetch( DefaultTechDocsCollatorFactory.constructDocsIndexUrl( techDocsBaseUrl, @@ -181,7 +201,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { ), { headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${techdocsToken}`, }, }, ); diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index b87bd15f7d..7ef7b7d2c5 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -56,7 +56,7 @@ "lodash": "^4.17.21", "lunr": "^2.3.9", "ndjson": "^2.0.0", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 5e3e6f9bb3..ef8bde1043 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -42,7 +42,7 @@ "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react-hook-form": "^7.12.2", "react-use": "^17.2.4", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "zen-observable": "^0.10.0" }, "devDependencies": { diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index f8424ed0c1..4fcca7b0e6 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -39,7 +39,7 @@ "express-promise-router": "^4.1.0", "http-proxy-middleware": "^2.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "ws": "^8.14.2", "yn": "^4.0.0" diff --git a/plugins/signals-node/package.json b/plugins/signals-node/package.json index c00696893d..03ef61739f 100644 --- a/plugins/signals-node/package.json +++ b/plugins/signals-node/package.json @@ -41,7 +41,7 @@ "@backstage/plugin-events-node": "workspace:^", "@backstage/types": "workspace:^", "express": "^4.17.1", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "ws": "^8.14.2" } } diff --git a/plugins/signals/package.json b/plugins/signals/package.json index 22cc82ef54..5f2f4222c2 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -40,7 +40,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.61", "react-use": "^17.2.4", - "uuid": "^8.0.0" + "uuid": "^9.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index d39f8a3d6d..b1d84acae1 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -56,7 +56,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "semver": "^7.5.3", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins_with_config.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins_with_config.yml new file mode 100644 index 0000000000..6c945162d7 --- /dev/null +++ b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins_with_config.yml @@ -0,0 +1,9 @@ +site_name: Test site name +site_description: Test site description +docs_dir: docs/ +plugins: + - not-techdocs-core + - also-not-techdocs-core + - custom-plugin: + with: + configuration: 1 diff --git a/plugins/techdocs-node/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts index 24d9b3effb..1b47cfb05c 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -81,6 +81,12 @@ const mkdocsYmlWithoutPlugins = fs.readFileSync( const mkdocsYmlWithAdditionalPlugins = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_additional_plugins.yml'), ); +const mkdocsYmlWithAdditionalPluginsWithConfig = fs.readFileSync( + resolvePath( + __filename, + '../__fixtures__/mkdocs_with_additional_plugins_with_config.yml', + ), +); const mkdocsYmlWithEnvTag = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_env_tag.yml'), ); @@ -321,6 +327,8 @@ describe('helpers', () => { 'mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins, 'mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins, 'mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins, + 'mkdocs_with_additional_plugins_with_config.yml': + mkdocsYmlWithAdditionalPluginsWithConfig, }); }); it('should not add additional plugins if techdocs exists already in mkdocs file', async () => { @@ -386,6 +394,28 @@ describe('helpers', () => { expect(parsedYml.plugins).toContain('techdocs-core'); expect(parsedYml.plugins).toContain('custom-plugin'); }); + it('should not overwrite config when defaults are added', async () => { + await patchMkdocsYmlWithPlugins( + mockDir.resolve('mkdocs_with_additional_plugins_with_config.yml'), + mockLogger, + ['techdocs-core', 'custom-plugin'], + ); + + const updatedMkdocsYml = await fs.readFile( + mockDir.resolve('mkdocs_with_additional_plugins_with_config.yml'), + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: object[]; + }; + expect(parsedYml.plugins).toHaveLength(4); + expect(parsedYml.plugins).toContain('techdocs-core'); + // we want our original object with its properties to be preserved, and for the basic string form of the plugin + // to NOT be added as well. + expect(parsedYml.plugins).not.toContain('custom-plugin'); + expect(parsedYml.plugins).toContainEqual({ + 'custom-plugin': { with: { configuration: 1 } }, + }); + }); }); describe('patchIndexPreBuild', () => { diff --git a/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts b/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts index 945aab3884..762a1c267a 100644 --- a/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts +++ b/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts @@ -153,21 +153,30 @@ export const patchMkdocsYmlWithPlugins = async ( defaultPlugins: string[] = ['techdocs-core'], ) => { await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => { - // Modify mkdocs.yaml to contain the required default plugins + // Modify mkdocs.yaml to contain the required default plugins. + // If no plugins are defined we can just return the defaults. if (!('plugins' in mkdocsYml)) { mkdocsYml.plugins = defaultPlugins; return true; } - if ( - mkdocsYml.plugins && - !defaultPlugins.every(plugin => mkdocsYml.plugins!.includes(plugin)) - ) { - mkdocsYml.plugins = [ - ...new Set([...mkdocsYml.plugins, ...defaultPlugins]), - ]; - return true; - } - return false; + // Otherwise, check each default plugin and include it if necessary. + let changesMade = false; + + defaultPlugins.forEach(dp => { + // if the plugin isn't there as a string, and isn't there as an object (which may itself contain extra config) + // then we need to add it + if ( + !( + mkdocsYml.plugins!.includes(dp) || + mkdocsYml.plugins!.some(p => p.hasOwnProperty(dp)) + ) + ) { + mkdocsYml.plugins = [...new Set([...mkdocsYml.plugins!, dp])]; + changesMade = true; + } + }); + + return changesMade; }); }; diff --git a/plugins/vault/src/api.test.ts b/plugins/vault/src/api.test.ts index 7ecd09b6f4..777137d0e7 100644 --- a/plugins/vault/src/api.test.ts +++ b/plugins/vault/src/api.test.ts @@ -110,7 +110,7 @@ describe('api', () => { it('should throw an error if the Vault API responds with a non-successful HTTP status code', async () => { await expect(api.listSecrets('test/error')).rejects.toThrow( - 'Request failed with 400 Error', + 'Request failed with 400 Bad Request', ); }); }); diff --git a/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx b/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx index 9450ddc41d..ec62ea8757 100644 --- a/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx +++ b/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/test-utils'; import { ComponentEntity } from '@backstage/catalog-model'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { EntityVaultCard } from './EntityVaultCard'; import { EntityProvider } from '@backstage/plugin-catalog-react'; @@ -45,8 +45,11 @@ describe('EntityVaultCard', () => { , ); - expect( - rendered.getByText(/Add the annotation to your Component YAML/), - ).toBeInTheDocument(); + + await waitFor(() => + expect( + rendered.getByText(/Add the annotation to your Component YAML/), + ).toBeInTheDocument(), + ); }); }); diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx index 5e191df9eb..14963ad13b 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx @@ -161,7 +161,7 @@ describe('EntityVaultTable', () => { expect( rendered.getByText( - /Unexpected error while fetching secrets from path \'test\/error\'\: Request failed with 400 Error/, + /Unexpected error while fetching secrets from path \'test\/error\'\: Request failed with 400 Bad Request/, ), ).toBeInTheDocument(); }); diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index 119dd6bef4..9a9336b3e9 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -32,7 +32,11 @@ const IGNORED_WHEN_LISTING = [ /^docs[/\\]reference[/\\]/, ]; -const IGNORED_WHEN_EXPLICIT = [/^.*[/\\]knip-report\.md$/]; +const IGNORED_WHEN_EXPLICIT = [ + /^ADOPTERS\.md$/, + /^OWNERS\.md$/, + /^.*[/\\]knip-report\.md$/, +]; const rootDir = resolvePath(__dirname, '..'); @@ -120,7 +124,6 @@ async function main() { const relativePaths = absolutePaths .map(path => relativePath(rootDir, path)) .filter(path => !IGNORED_WHEN_EXPLICIT.some(pattern => pattern.test(path))); - const success = await runVale( relativePaths.length === 0 ? await listFiles() : relativePaths, ); diff --git a/yarn.lock b/yarn.lock index 4583c60afd..754d563751 100644 --- a/yarn.lock +++ b/yarn.lock @@ -363,569 +363,569 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-cognito-identity@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/client-cognito-identity@npm:3.515.0" +"@aws-sdk/client-cognito-identity@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: e254357719b355a7c6cdd718c3896aca9971ea9479887c221841ec2c6e91f15b880f6a5d1ebfc62af223b4d02a0d4a882aa8394b436f9b544658ee87a481e1ac + checksum: e664da040d7688b97603dc49310c40acaae95c87dd2b32786664d5ee553a5e4079677b5ebcf5e85137434eafebe20c1d9f6f49b05926394d5b82cac2bcc2bbb8 languageName: node linkType: hard "@aws-sdk/client-eks@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/client-eks@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/client-eks@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 - "@smithy/util-waiter": ^2.1.1 + "@smithy/util-waiter": ^2.1.2 tslib: ^2.5.0 uuid: ^9.0.1 - checksum: 38366b504f5cda083637801b8a89cb03fb05fca56e6266a18d4fa4772a5556a461efcb82c8ed9690d8ba4c49e2ec2bdd71c7d3f987d2e7a85506439de2199ff6 + checksum: c31d933e4f64173276e6c3a9cba1e699fc25d8b43daff15cc52e23c4bbfdbac63f7e6cc8c55910bde551b568c6c7f928b432d518fe6251c5d1fad65854d44861 languageName: node linkType: hard "@aws-sdk/client-organizations@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/client-organizations@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/client-organizations@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 4b947f1fc5ca196b007855cc5d75accb82825adfe4dea370d181b9605bfe827ddc0f6cb073d7ef7f768a523be4e393a2dc1e28a999d118bda050f354997e8f60 + checksum: 1191e702f2123f2316f175dc81e3c00df05c79a53be709f166834c8425a600cff4d61bb0db7befc1fa3fc801dcccc05faab87f74adf0100b8428b4ee26c0a963 languageName: node linkType: hard "@aws-sdk/client-s3@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/client-s3@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/client-s3@npm:3.521.0" dependencies: "@aws-crypto/sha1-browser": 3.0.0 "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/middleware-bucket-endpoint": 3.515.0 - "@aws-sdk/middleware-expect-continue": 3.515.0 - "@aws-sdk/middleware-flexible-checksums": 3.515.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-location-constraint": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-sdk-s3": 3.515.0 - "@aws-sdk/middleware-signing": 3.515.0 - "@aws-sdk/middleware-ssec": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/signature-v4-multi-region": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@aws-sdk/xml-builder": 3.496.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/eventstream-serde-browser": ^2.1.1 - "@smithy/eventstream-serde-config-resolver": ^2.1.1 - "@smithy/eventstream-serde-node": ^2.1.1 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-blob-browser": ^2.1.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/hash-stream-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/md5-js": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/middleware-bucket-endpoint": 3.521.0 + "@aws-sdk/middleware-expect-continue": 3.521.0 + "@aws-sdk/middleware-flexible-checksums": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-location-constraint": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-sdk-s3": 3.521.0 + "@aws-sdk/middleware-signing": 3.521.0 + "@aws-sdk/middleware-ssec": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/signature-v4-multi-region": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@aws-sdk/xml-builder": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/eventstream-serde-browser": ^2.1.2 + "@smithy/eventstream-serde-config-resolver": ^2.1.2 + "@smithy/eventstream-serde-node": ^2.1.2 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-blob-browser": ^2.1.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/hash-stream-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/md5-js": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-retry": ^2.1.1 - "@smithy/util-stream": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-retry": ^2.1.2 + "@smithy/util-stream": ^2.1.2 "@smithy/util-utf8": ^2.1.1 - "@smithy/util-waiter": ^2.1.1 + "@smithy/util-waiter": ^2.1.2 fast-xml-parser: 4.2.5 tslib: ^2.5.0 - checksum: f61f91fb45500108520357e61a018975f4e4a49df378fa16ad0fc15b59560bdfcc11c6cb7d95ad1c4b6e0607e99788662e4013d8d529af75a36dffa5c8bfe5ca + checksum: bf3c5d6a42df6812f5645751bdbbb6cd69f8b28c3f0dd5ba7697a631e7573494e7cdc2bdc8081e49e5159a73c5a32259575a9d79f170753e8fbc53613e3ecc9c languageName: node linkType: hard "@aws-sdk/client-sqs@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/client-sqs@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/client-sqs@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-sdk-sqs": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/md5-js": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-sdk-sqs": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/md5-js": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: e92bbea7c7453cee74898ffde1092437693eb19b3a623243913805229ab6b1ec7905735a2dd58476915d574beeca2675f00eb89b5de8c3f099becc68c9ade901 + checksum: 9f65ffa57f0da1279f00754234e504b9f2250085a2bc843605f591b54a67929b73ee4402a40a291a5f9ce9e9936ad1d31676e37a12bb30c4c24f2bcb17483c5b languageName: node linkType: hard -"@aws-sdk/client-sso-oidc@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/client-sso-oidc@npm:3.515.0" +"@aws-sdk/client-sso-oidc@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/client-sso-oidc@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 peerDependencies: - "@aws-sdk/credential-provider-node": ^3.515.0 - checksum: f220a9ba8542460b2aa91ad060302fb9e68bdf096ecca2ec1d6e525f4df1036b330cb85d20bac3e8399276c0d8d8d388b3f2191b58804919c68369afac0be37b + "@aws-sdk/credential-provider-node": ^3.521.0 + checksum: da6b724cd91f128192eba0bbf0827c7e6fccb30f899240eb908eb62d0a57437e1ac7e28097d31b8af1d616a35a37af97f8dcf9137230cb98e1b6cc33a9f38d36 languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/client-sso@npm:3.515.0" +"@aws-sdk/client-sso@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/client-sso@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 12287dfa469fb2c6b5bedd3cbd37f7416f8234669b5ed0ff38cb1217d746ba6a5e6ff227b091a7751d1c20489a6b8bd93bcbed8f394cb4c51b6bebb9a9f79108 + checksum: 1035c3beb9d090d6f3858be022c66127d246e2b6c88336c808300814628acd18666cb76f6f6dfcf622b3b67dd195a29230602608bea9b40dde54b98079331359 languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.515.0, @aws-sdk/client-sts@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/client-sts@npm:3.515.0" +"@aws-sdk/client-sts@npm:3.521.0, @aws-sdk/client-sts@npm:^3.350.0": + version: 3.521.0 + resolution: "@aws-sdk/client-sts@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 fast-xml-parser: 4.2.5 tslib: ^2.5.0 peerDependencies: - "@aws-sdk/credential-provider-node": ^3.515.0 - checksum: 9af6a2484909e88a83c411551d55ad149c80a8f449c2e54c499769535243602a6283cd71f6a0cf975b295a321a74e90eb95f6659bba93bae3d12e2186e7545f4 + "@aws-sdk/credential-provider-node": ^3.521.0 + checksum: 1ca480532746fa6d81bf84bebf6e38ab3d2565789654465ec22bd8c34daf2c64165a90d70e29da8170da7ba9a18041c3da4a037f37689939798bf39c03348a50 languageName: node linkType: hard -"@aws-sdk/core@npm:3.513.0": - version: 3.513.0 - resolution: "@aws-sdk/core@npm:3.513.0" +"@aws-sdk/core@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/core@npm:3.521.0" dependencies: - "@smithy/core": ^1.3.2 - "@smithy/protocol-http": ^3.1.1 + "@smithy/core": ^1.3.3 + "@smithy/protocol-http": ^3.2.0 "@smithy/signature-v4": ^2.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 94a41263e5d0c754f4d6d603572704822b570d5fc5ed450c8eb461b989198b625d2c115a470b087defe2c6c45b9442527062382c9bb1ca32842332317300b2fe + checksum: 43d02d64563b6fc5c55be1fd62dc67a95b862e8355d9c8d574c09fdc3668f7524723aec7a23ce29a40188d56479cba705dd8470787bebfd30b98c046f3b29606 languageName: node linkType: hard -"@aws-sdk/credential-provider-cognito-identity@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.515.0" +"@aws-sdk/credential-provider-cognito-identity@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.521.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-cognito-identity": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: cb8cb5fa19b3e10e0a816eddda0a56a412da6bc68eba4807698ed94ef6bf2f3de288d959ced099b399dfc29f08154c84f8c025816e94226e5fb21c605e710542 + checksum: fce4a6b934839cb347fa603eb2c50f70e96e939528d3747492658de98e27d4f412256a40d3b63bb8d983ba81b695c7fff718632bd311c3d74e14194ea4553d95 languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.515.0" +"@aws-sdk/credential-provider-env@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 3573bc3f1aa89bc8eedb9eb39c8c1d501a68aec5eb059364a1091c8bf10dfda9cfbd78ee49d3ad25ec1012f765a4464363c4cd70997e94005fd21245871a4229 + checksum: 5b217fa1fc86f1d553bab39ac30942e06dddbffe3061cfcafb978f61740749eec9155271ba241df8889c49e73b498a2e4036b384b8b6de8c63da8f5682228d41 languageName: node linkType: hard -"@aws-sdk/credential-provider-http@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-http@npm:3.515.0" +"@aws-sdk/credential-provider-http@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/node-http-handler": ^2.3.1 + "@aws-sdk/types": 3.521.0 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/node-http-handler": ^2.4.0 "@smithy/property-provider": ^2.1.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/util-stream": ^2.1.1 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/util-stream": ^2.1.2 tslib: ^2.5.0 - checksum: d13943dc7a83c9c129dd03a8b337b7753c791441d65a894085e00146d807738b420b9127f570a32497665be4f6e1cc8eeefd7cb1b013a04789d874c8b23b829f + checksum: b953861a460c2c871a390036e6ed5acdfd545e23204c56d2ae7835c46a43a598103175b9071107f9e817b9c386edc98a3b5e677ecc42b313b2d48cea2d11ca8e languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.515.0" +"@aws-sdk/credential-provider-ini@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.521.0" dependencies: - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/credential-provider-env": 3.515.0 - "@aws-sdk/credential-provider-process": 3.515.0 - "@aws-sdk/credential-provider-sso": 3.515.0 - "@aws-sdk/credential-provider-web-identity": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/credential-provider-env": 3.521.0 + "@aws-sdk/credential-provider-process": 3.521.0 + "@aws-sdk/credential-provider-sso": 3.521.0 + "@aws-sdk/credential-provider-web-identity": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/credential-provider-imds": ^2.2.1 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c136d4257460be8331d7645854b7e3c91205a1eb12efd3dcbcd84501c787085292d1ccc4578c4776308d91748bde5af2c6eaa873b56aed83ab472a878a2d8883 + checksum: 41c9eda9ec49927999aab9137c46392576b84611c434c369c004eb99fcad212188dcf8dbc3bb9388a65b0077b52835b5219c0f68cfd640c2119772678608fad1 languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:3.515.0, @aws-sdk/credential-provider-node@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.515.0" +"@aws-sdk/credential-provider-node@npm:3.521.0, @aws-sdk/credential-provider-node@npm:^3.350.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.521.0" dependencies: - "@aws-sdk/credential-provider-env": 3.515.0 - "@aws-sdk/credential-provider-http": 3.515.0 - "@aws-sdk/credential-provider-ini": 3.515.0 - "@aws-sdk/credential-provider-process": 3.515.0 - "@aws-sdk/credential-provider-sso": 3.515.0 - "@aws-sdk/credential-provider-web-identity": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/credential-provider-env": 3.521.0 + "@aws-sdk/credential-provider-http": 3.521.0 + "@aws-sdk/credential-provider-ini": 3.521.0 + "@aws-sdk/credential-provider-process": 3.521.0 + "@aws-sdk/credential-provider-sso": 3.521.0 + "@aws-sdk/credential-provider-web-identity": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/credential-provider-imds": ^2.2.1 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c51f267c61de0d82afe47d97cdf58971e3b8eec6e7364fe28d3867addd65e156358b96c39a335fad521e9a6925b349a967ae3eed1aa6e9cb4bcc1f0931e3ed50 + checksum: eaa75f81151113f84ccd7e26964a139fd598645f93074431ec5979a5ebf926df1da8659ecb46b4d79e4b83de30f7136954ea72fadc60a4e8bc5e828f193b7556 languageName: node linkType: hard -"@aws-sdk/credential-provider-process@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.515.0" +"@aws-sdk/credential-provider-process@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 11159b4c9502218ec6cba9a46ddc120e53aec7f04507c14d4e99a186073cfd363af438c623705890a2e8f6cc792475ebd14c82766dad82dabf7f20d9708f7faf + checksum: 7770461063e9c330331f48401a16d0f792c97c5392b2f393e09f1be6c5ec0f97316c7fa11c2ebfc7392984acced03d5205558b264ed6417697993a9a466185b2 languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.515.0" +"@aws-sdk/credential-provider-sso@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.521.0" dependencies: - "@aws-sdk/client-sso": 3.515.0 - "@aws-sdk/token-providers": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-sso": 3.521.0 + "@aws-sdk/token-providers": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: fbe1eebc50e9bd3715bca4e6ee1a2922cf1ea383953730a6bd3b88b12f23a95cb3ff0edaf8578c81156ef65648eb0295f5c39ed6ae2c8e16b6ce9d4c46f207e9 + checksum: 76fbed5935eb4f7cd244f9d90bb5b9f1069226ecaaf03d69f16b4f607864541cc6b8e6d038a07eef27acff75df8f539c3c61d6b672f5bda4db1a1a3966815566 languageName: node linkType: hard -"@aws-sdk/credential-provider-web-identity@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.515.0" +"@aws-sdk/credential-provider-web-identity@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.521.0" dependencies: - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: f0a7e9855f78849143c3139c613cc1531a88272f3ec039d23ac9366b94495a3e07212ade9541e56c93560ad9f58600376e9de38706a4cd97aaf5f400911361cd + checksum: 4e9360d0e6a55b7f60d837addb87dbd5d804e4202c2fadc75d11196a54dbb6099fa977fb0a51191d8c62373f17279c379a304b697ddd15abe5508a30c2a34556 languageName: node linkType: hard "@aws-sdk/credential-providers@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-providers@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/credential-providers@npm:3.521.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.515.0 - "@aws-sdk/client-sso": 3.515.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/credential-provider-cognito-identity": 3.515.0 - "@aws-sdk/credential-provider-env": 3.515.0 - "@aws-sdk/credential-provider-http": 3.515.0 - "@aws-sdk/credential-provider-ini": 3.515.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/credential-provider-process": 3.515.0 - "@aws-sdk/credential-provider-sso": 3.515.0 - "@aws-sdk/credential-provider-web-identity": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-cognito-identity": 3.521.0 + "@aws-sdk/client-sso": 3.521.0 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/credential-provider-cognito-identity": 3.521.0 + "@aws-sdk/credential-provider-env": 3.521.0 + "@aws-sdk/credential-provider-http": 3.521.0 + "@aws-sdk/credential-provider-ini": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/credential-provider-process": 3.521.0 + "@aws-sdk/credential-provider-sso": 3.521.0 + "@aws-sdk/credential-provider-web-identity": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/credential-provider-imds": ^2.2.1 "@smithy/property-provider": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 8890e83fc19072c51cee8d50ee7168800b9775aa00bcb7a0feb836e60fbae0fb8ba55133d256719033cd7ccda0f5881350474ba3f4769016f01ce7de4ef7b6e5 + checksum: c4b3ca40a4e7a9843847f81f0a3f3ac568986e87054e44df8cad8ddf3f8df6314c4e67ca27b150b05cdfe6e96ccca32457654a121b09a9ba017819b9000d534b languageName: node linkType: hard @@ -951,34 +951,34 @@ __metadata: linkType: hard "@aws-sdk/lib-storage@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/lib-storage@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/lib-storage@npm:3.521.0" dependencies: "@smithy/abort-controller": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/smithy-client": ^2.3.1 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/smithy-client": ^2.4.0 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 tslib: ^2.5.0 peerDependencies: "@aws-sdk/client-s3": ^3.0.0 - checksum: b4d7b3783508ce3ef93150b2a249490c5af803d02d91ce81aebf0ec055870aae3260d4d66e9bf1d4234fb61d11cfaa54f2916c9c2572cf4cb6ee1b15993c41b5 + checksum: b55cdbe2744970f863f30e84ba39dc25d123032cf5e7b11a3fb8d6dc0aff7f3311f00112c72b686f4833ff6494c3b1c346cdb03cc5727652933ebd98dc1f8cf7 languageName: node linkType: hard -"@aws-sdk/middleware-bucket-endpoint@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.515.0" +"@aws-sdk/middleware-bucket-endpoint@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@aws-sdk/util-arn-parser": 3.495.0 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 "@smithy/util-config-provider": ^2.2.1 tslib: ^2.5.0 - checksum: 8ecec09ac50c33a24178e73be6b4238e5047984eebf54c4786392e82d2b1cdd09807ee97104f603d22890a58657335843f9587c66e544c2ffbb9a25f3e3bf065 + checksum: 407ea1c7d64159fa86ccc002333c646b841fa7cbec70400798af3b688418a1f44bea31dac4e462383f9221144b113c9ee33c5e585e1ef9d7d207cb91a2439cc7 languageName: node linkType: hard @@ -995,108 +995,108 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-expect-continue@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.515.0" +"@aws-sdk/middleware-expect-continue@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c989e0e55f51c631f914b444a0e3a6a81c79c1ad8046346c16a4ccfbb77aeaf48ba948a6b8185fc36c00abf07951cc374c3673da6d23d7de6cd3ec8d5b4d840e + checksum: 28ed930f9e4d9d90d705c371c2ac3055fa1fdb8c6de44d1db899f6ade1231cbe3de7d4f6e7fa7dae41fe0d79d5ee5b068ba8ecaa885dc6a2a6100cc230b0847a languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.515.0" +"@aws-sdk/middleware-flexible-checksums@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.521.0" dependencies: "@aws-crypto/crc32": 3.0.0 "@aws-crypto/crc32c": 3.0.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@smithy/is-array-buffer": ^2.1.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 89f2df7a3dd40c174586aec1349e4e144825cffb252df843b115390702523dc6cfb745756a69a1ab1e77e44340fd55bed04be3856fe96d937af5702bcbcf869b + checksum: 60a77546090174ca7cfd8d6894c6d4583ae5ad311f85dc407c1d98128fa16322850a7284a4f75260d4ab5529296110b0952997c1726c325813988c14e2aba792 languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.515.0" +"@aws-sdk/middleware-host-header@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: ff066cf47b0ba2c64bd70efdec795ac2da8bad7ba8dd44913c98f42b153ca6e753b13b6c1ef7075499590279a5cc49b5a60511dae4512dcdb11a62a0e67fa061 + checksum: e0e0597f436bce61c9fc2598d65db68610d8a4a8576e2d9073d8ffa0fe3099a76570c997fc9ab46ef18b1530102ef73101db04d28ce0a40f40ca76bf502d8db9 languageName: node linkType: hard -"@aws-sdk/middleware-location-constraint@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.515.0" +"@aws-sdk/middleware-location-constraint@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c3e7c7b51c276eba7ceef05c18416d2cca976c83ac9db227877090c33f676b50b0c90ceaf5d924cd9894a4c2f44c0a2d06ec93da753ac646fecb68d5facef570 + checksum: 62b8ac417945c826a3042369116250e17b31df2f7f949494320de695985db0801986968503b366965dd0e1beaf25bd37890261dbc2e5fdbf2b0f3cf581ace603 languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-logger@npm:3.515.0" +"@aws-sdk/middleware-logger@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-logger@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 32d251e77f43593ffdd192a4d0628f33773e29c14a3001a4c6519553e94958edbd0fb8e6954a65d1180b0caa16cafe9fc9b362d1ab663db1d1eac84b15667645 + checksum: 9ff749309bd457be1356d3efea53d9067c15baa631d4ba7d874172f087030d4ccbb7df38ccdecb6823944af2732b01caa3cd495a245fa5a4975a5442e2059532 languageName: node linkType: hard -"@aws-sdk/middleware-recursion-detection@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.515.0" +"@aws-sdk/middleware-recursion-detection@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 23c4a1e4d7de86196acfcfbc84bea84c8c3211c4831fdc7c975a6388022037bd5baa4e5809dca188631f06726b51fcf85af358f9ef526e255dc494b368d0da0c + checksum: a097d83c411944d30105a997520791d9a16800a4d5b5b8a77ef5dd8edb3616ecf358a669a4c116314bf60d2ab1c23d54f0c1d79a2e2bcfa38b84a5ec3b418b86 languageName: node linkType: hard -"@aws-sdk/middleware-sdk-s3@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.515.0" +"@aws-sdk/middleware-sdk-s3@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@aws-sdk/util-arn-parser": 3.495.0 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/protocol-http": ^3.1.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/protocol-http": ^3.2.0 "@smithy/signature-v4": ^2.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 "@smithy/util-config-provider": ^2.2.1 tslib: ^2.5.0 - checksum: cb67334b30eee8fcf52637407cab2787353e463bdf5a99a33966f0e765f6f8fc687c4bba2b63ec7a4374b2ab544be807e1849bd910e6f163ff43d036f026e7e1 + checksum: f29d1eed5f3f4de2bb0e85bd572d31f41f494f83f3903edaa45bb79024b6e7cca4bc404a57bb2d69dad58ea1ab92942016a7cd9a5295a54e96851e44a9ce649f languageName: node linkType: hard -"@aws-sdk/middleware-sdk-sqs@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.515.0" +"@aws-sdk/middleware-sdk-sqs@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 "@smithy/util-hex-encoding": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 67a7d9ed3e975a3fd83266f18ce2d94e3dc457d7001af7031e732b2d96135678dd3323ecae56ffe214f3dbe23d1df295a0cd0d21df7f6b9c98699e6a9b4704ca + checksum: 4fcb19c74ce0a667b78fb053b25a4c6c57d84f8eb2167045922a6310d7ecc9def78abe6f2f310ac78327dc41f54347df0497098db00b571cf26029bee7eff1f7 languageName: node linkType: hard @@ -1110,42 +1110,42 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-signing@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-signing@npm:3.515.0" +"@aws-sdk/middleware-signing@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-signing@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 - "@smithy/protocol-http": ^3.1.1 + "@smithy/protocol-http": ^3.2.0 "@smithy/signature-v4": ^2.1.1 - "@smithy/types": ^2.9.1 - "@smithy/util-middleware": ^2.1.1 + "@smithy/types": ^2.10.0 + "@smithy/util-middleware": ^2.1.2 tslib: ^2.5.0 - checksum: 7ee85c70a81b85e455c4411caad79c7b41a0a0fd9696feead394a31ca360b096c74f7b2b6b5d449fcfa622659305618e24a49d15c45cab3afa54503459a9b24a + checksum: 545225d39d0e6133f14ca6ac721b78293b7ae2b522d9229f3c4afa00efe5ccd38c2b11a508bbec050f2b91a68f51fe6260db57853e8c28f7f80d09b93ef6120f languageName: node linkType: hard -"@aws-sdk/middleware-ssec@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-ssec@npm:3.515.0" +"@aws-sdk/middleware-ssec@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-ssec@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 3a91ebaf128ff63665f9aa69b9b2d475ebb8e519da2e94a142bcea7f9b173be52a0476b814e90e88e0936db3371e3c1c090b4e44018f006e1cfc37d2631a36d8 + checksum: 86b31dcc825d194898dbcc72e9a7590e9693bfbe8d3e0707f7b82814f735dfb73c06f77d68785b061faf173b8a1da6f47f98564602437532c75f8da77f820aca languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.515.0" +"@aws-sdk/middleware-user-agent@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: fd601cb0367d42e38b71494c773d82bde8970f9aafbdbf18d7cadc25732ecc2b787f0cfef2110755d0cef73d6aa3ce2ca77dc5353854bedaf392a69019b39ac2 + checksum: 5d1461de4d6d6c7cfd6f7bca9753f3c8340b3bb27638940000d14811ff9a3e63ffecc9c9e4b8784e53a834815d58f902ae9e4a07b60b62d56629359de80fbe6c languageName: node linkType: hard @@ -1193,31 +1193,31 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/region-config-resolver@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/region-config-resolver@npm:3.515.0" +"@aws-sdk/region-config-resolver@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/types": ^2.10.0 "@smithy/util-config-provider": ^2.2.1 - "@smithy/util-middleware": ^2.1.1 + "@smithy/util-middleware": ^2.1.2 tslib: ^2.5.0 - checksum: 0ed7fbd6390baebdf511b30877236fa8be8716e0162e2c9e0138c9b41ebda7d99a6f3d6cf66cb4af24761631c2c29102ecfe7a5f08894e1de3c98ca6a135fa74 + checksum: ce0ec289d6ca59747c1e96dd3b45f11fe690fc2b0407beacadfd07e04258474c2be51a265851ef7bca0feb5f0e7ba6520ee2c5de2b112dff1fbd5c37901e2e72 languageName: node linkType: hard -"@aws-sdk/signature-v4-multi-region@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.515.0" +"@aws-sdk/signature-v4-multi-region@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.521.0" dependencies: - "@aws-sdk/middleware-sdk-s3": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@smithy/protocol-http": ^3.1.1 + "@aws-sdk/middleware-sdk-s3": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@smithy/protocol-http": ^3.2.0 "@smithy/signature-v4": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 1f780409af431b3ac91beee294cf4af74fff9f3dffc21ddd1c8a1782a01916da162ceec27c133a9e86452279bdf0005f9118809820d557a248613cd44f0f7ec3 + checksum: 312334438f12927e842d0a5c1d3fd837bedb54bc96caa7a7c0f1168a87532ee1b2e90090a2e1369c28cc0dc4b24c79ae7bf9aca66cbb811b61c8fdda775c4581 languageName: node linkType: hard @@ -1237,17 +1237,17 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/token-providers@npm:3.515.0" +"@aws-sdk/token-providers@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/token-providers@npm:3.521.0" dependencies: - "@aws-sdk/client-sso-oidc": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-sso-oidc": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: ab51c440da9772d0ee58948241b975705c171395cf1bad81a4ffd8f11f34106af54dcba930e360fb19489beedc1106ac14432bd27abdfe4b7536dc2113841027 + checksum: e34671eaab24dac569d0a98a87b21e2a6ff0960cdc931276f446a31310bff343af33dcf742738a02c9eb8e01226981ab64abd8e59e7758ed695f0045bb524af6 languageName: node linkType: hard @@ -1261,13 +1261,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/types@npm:3.515.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0": - version: 3.515.0 - resolution: "@aws-sdk/types@npm:3.515.0" +"@aws-sdk/types@npm:3.521.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0": + version: 3.521.0 + resolution: "@aws-sdk/types@npm:3.521.0" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 0874f1814b58eae6e7115c3d08c2bc56e558e73d1ff8c5f833a73b4a0f76a42743c83c36a4b2759177e41b1feff065e85450f7bc235a087b94e67db12f87d298 + checksum: 28d9ab39ad19e74ca721100131152bec975cea3c78e5013e70e9684b051c5115623430a923f0e92426b298033be94ebd554925ec4a5fb64273c48df90ea6c6eb languageName: node linkType: hard @@ -1301,15 +1301,15 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-endpoints@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/util-endpoints@npm:3.515.0" +"@aws-sdk/util-endpoints@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/util-endpoints@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/types": ^2.9.1 - "@smithy/util-endpoints": ^1.1.1 + "@aws-sdk/types": 3.521.0 + "@smithy/types": ^2.10.0 + "@smithy/util-endpoints": ^1.1.2 tslib: ^2.5.0 - checksum: 1ab8fcd3054dc0366f10813a01130d05f4ba33f1488c1a168f44881cb24f3fbc2393111b7b0fd4dc06c852e4c9a5bbe8a82717b72229b0977cdba8a631ddeee1 + checksum: a8f01159d4114a7893200a3a782ccec091da7a46b4de5bb1c4db253ad836de123fbb314c376798044119b49f21b9d3bfd61eecb4328da9a73279719079baba48 languageName: node linkType: hard @@ -1361,32 +1361,32 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.515.0" +"@aws-sdk/util-user-agent-browser@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/types": ^2.10.0 bowser: ^2.11.0 tslib: ^2.5.0 - checksum: 40f518006cb7e76d06d83dcf05222b0b0ff47c10b63149cd5db2c0c1db79c8eff34bd582e89c748897bc11697b7b357bdca77d569f57ad0b2081c088752d601f + checksum: 1938f4e00873a3d0ba55988d562fe987352abf6c57fef7c27dc28f561a7752af73d34883eb89eba1173b42b8d31ed0d0a4fc2592b46c28e775545bdb27b0bc80 languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.515.0" +"@aws-sdk/util-user-agent-node@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 peerDependencies: aws-crt: ">=1.0.0" peerDependenciesMeta: aws-crt: optional: true - checksum: 4e91d9cd5bbe4aa8321417ea1bd9caf3229416ee624b7f67b5206b284a539116692412ca41d60dcb5759b841fed7d9fb570915566d1f7e620578657f89548a23 + checksum: d78a47e32fef990da97635af88484649636bf452320ce80ed6376302d18014f7b191f5da3c096e44aa4f9054639c31c3b2e62d06e7708f747ef7847bad019aa7 languageName: node linkType: hard @@ -1409,13 +1409,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/xml-builder@npm:3.496.0": - version: 3.496.0 - resolution: "@aws-sdk/xml-builder@npm:3.496.0" +"@aws-sdk/xml-builder@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/xml-builder@npm:3.521.0" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 42d9d60c1c7f8a22f6a64ac36ba9d5ccff200ce963beebb142ad4708d2486fe29b61ecb37bbccd6f0019e25107aa2327e6d8550d30214b4895e7219ccb8661c8 + checksum: 33a86fdcf93029706725f829b9cea1846c8c51d0170410e19c75564256cb5825fb4c9e65ecbb7930c5ee14855bb63425b596fc2660978c5724197fee1b09c8f5 languageName: node linkType: hard @@ -3144,7 +3144,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.8, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.8, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.23.8 resolution: "@babel/runtime@npm:7.23.8" dependencies: @@ -3340,10 +3340,10 @@ __metadata: raw-body: ^2.4.1 supertest: ^6.1.3 tar: ^6.1.12 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 winston-transport: ^4.5.0 - yauzl: ^2.10.0 + yauzl: ^3.0.0 yn: ^4.0.0 peerDependencies: pg-connection-string: ^2.3.0 @@ -3462,7 +3462,7 @@ __metadata: knex: ^3.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - uuid: ^8.0.0 + uuid: ^9.0.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 zod: ^3.22.4 @@ -3483,6 +3483,7 @@ __metadata: "@backstage/types": "workspace:^" "@types/supertest": ^2.0.8 better-sqlite3: ^9.0.0 + cookie: ^0.6.0 express: ^4.17.1 fs-extra: ^11.0.0 knex: ^3.0.0 @@ -3490,9 +3491,9 @@ __metadata: mysql2: ^3.0.0 pg: ^8.11.3 supertest: ^6.1.3 - testcontainers: ^8.1.2 + testcontainers: ^10.0.0 textextensions: ^5.16.0 - uuid: ^8.0.0 + uuid: ^9.0.0 peerDependencies: "@types/jest": "*" languageName: unknown @@ -4225,12 +4226,11 @@ __metadata: "@backstage/plugin-adr-common": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@types/express": ^4.17.6 - "@types/marked": ^5.0.0 "@types/supertest": ^2.0.8 express: ^4.17.1 express-promise-router: ^4.1.0 luxon: ^3.0.0 - marked: ^4.0.14 + marked: ^12.0.0 node-fetch: ^2.6.5 supertest: ^6.1.3 winston: ^3.2.1 @@ -4683,6 +4683,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth-backend-module-guest-provider@workspace:^, @backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + express: ^4.18.2 + passport-oauth2: ^1.7.0 + languageName: unknown + linkType: soft + "@backstage/plugin-auth-backend-module-microsoft-provider@workspace:^, @backstage/plugin-auth-backend-module-microsoft-provider@workspace:plugins/auth-backend-module-microsoft-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-microsoft-provider@workspace:plugins/auth-backend-module-microsoft-provider" @@ -4889,7 +4906,7 @@ __metadata: passport-oauth2: ^1.6.1 passport-onelogin-oauth: ^0.0.1 supertest: ^6.1.3 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -4919,7 +4936,7 @@ __metadata: node-fetch: ^2.6.7 passport: ^0.7.0 supertest: ^6.1.3 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 zod: ^3.22.4 zod-to-json-schema: ^3.21.4 @@ -5001,6 +5018,7 @@ __metadata: "@azure/arm-resourcegraph": ^4.2.1 "@azure/identity": ^4.0.0 "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" @@ -5008,6 +5026,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-azure-sites-common": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" "@types/express": ^4.17.6 @@ -5237,7 +5256,7 @@ __metadata: aws-sdk-client-mock-jest: ^3.0.0 luxon: ^3.0.0 p-limit: ^3.0.2 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 yaml: ^2.0.0 languageName: unknown @@ -5259,7 +5278,7 @@ __metadata: luxon: ^3.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5304,11 +5323,11 @@ __metadata: "@backstage/plugin-bitbucket-cloud-common": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" luxon: ^3.0.0 msw: ^1.0.0 - uuid: ^8.0.0 - winston: ^3.2.1 + uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5330,7 +5349,7 @@ __metadata: luxon: ^3.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5371,7 +5390,7 @@ __metadata: luxon: ^3.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5418,7 +5437,7 @@ __metadata: minimatch: ^9.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5437,12 +5456,12 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@types/lodash": ^4.14.151 - "@types/uuid": ^8.0.0 + "@types/uuid": ^9.0.0 lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5470,7 +5489,7 @@ __metadata: express-promise-router: ^4.1.0 knex: ^3.0.0 luxon: ^3.0.0 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5491,7 +5510,7 @@ __metadata: "@types/lodash": ^4.14.151 ldapjs: ^2.2.0 lodash: ^4.17.21 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5519,7 +5538,7 @@ __metadata: node-fetch: ^2.6.7 p-limit: ^3.0.2 qs: ^6.9.4 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5565,7 +5584,7 @@ __metadata: luxon: ^3.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5628,7 +5647,7 @@ __metadata: "@types/glob": ^8.0.0 "@types/lodash": ^4.14.151 "@types/supertest": ^2.0.8 - "@types/uuid": ^8.0.0 + "@types/uuid": ^9.0.0 better-sqlite3: ^9.0.0 codeowners-utils: ^1.0.2 core-js: ^3.6.5 @@ -5646,7 +5665,7 @@ __metadata: p-limit: ^3.0.2 prom-client: ^15.0.0 supertest: ^6.1.3 - uuid: ^8.0.0 + uuid: ^9.0.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 yaml: ^2.0.0 @@ -6014,6 +6033,7 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" @@ -6030,7 +6050,7 @@ __metadata: express-promise-router: ^4.1.0 knex: ^3.0.0 supertest: ^6.1.6 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 xml2js: ^0.6.0 yn: ^4.0.0 @@ -6169,7 +6189,7 @@ __metadata: react-use: ^17.2.4 recharts: ^2.5.0 regression: ^2.0.1 - yup: ^0.32.9 + yup: ^1.0.0 peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 @@ -6391,7 +6411,6 @@ __metadata: "@backstage/types": "workspace:^" aws-sdk-client-mock: ^3.0.0 luxon: ^3.0.0 - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6404,7 +6423,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6417,7 +6435,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6430,7 +6447,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6445,7 +6461,6 @@ __metadata: "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" "@octokit/webhooks-methods": ^3.0.0 - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6459,7 +6474,6 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6495,6 +6509,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-events-node@workspace:plugins/events-node" dependencies: + "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" languageName: unknown @@ -7058,6 +7073,7 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" @@ -7449,7 +7465,7 @@ __metadata: luxon: ^3.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -7643,7 +7659,7 @@ __metadata: msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -7673,7 +7689,7 @@ __metadata: "@backstage/test-utils": "workspace:^" knex: ^3.0.0 msw: ^1.0.0 - uuid: ^8.0.0 + uuid: ^9.0.0 languageName: unknown linkType: soft @@ -7699,6 +7715,7 @@ __metadata: "@testing-library/react": ^14.0.0 "@testing-library/user-event": ^14.0.0 "@types/react": ^16.13.1 || ^17.0.0 + lodash: ^4.17.21 msw: ^1.0.0 react-relative-time: ^0.0.9 react-use: ^17.2.4 @@ -7917,6 +7934,7 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" @@ -7949,7 +7967,7 @@ __metadata: "@backstage/types": "workspace:^" cross-fetch: ^4.0.0 msw: ^1.0.0 - uuid: ^8.0.0 + uuid: ^9.0.0 zod: ^3.22.4 languageName: unknown linkType: soft @@ -8020,7 +8038,7 @@ __metadata: knex: ^3.0.0 node-fetch: ^2.6.7 supertest: ^6.1.3 - uuid: ^8.2.0 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 zod: ^3.22.4 @@ -8089,7 +8107,7 @@ __metadata: "@types/express": ^4.17.6 "@types/http-proxy-middleware": ^0.19.3 "@types/supertest": ^2.0.8 - "@types/uuid": ^8.0.0 + "@types/uuid": ^9.0.0 "@types/yup": ^0.29.13 express: ^4.17.1 express-promise-router: ^4.1.0 @@ -8097,11 +8115,11 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 supertest: ^6.1.3 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 yaml: ^2.0.0 yn: ^4.0.0 - yup: ^0.32.9 + yup: ^1.0.0 languageName: unknown linkType: soft @@ -8194,6 +8212,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" azure-devops-node-api: ^12.0.0 yaml: ^2.0.0 languageName: unknown @@ -8211,6 +8230,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 node-fetch: ^2.6.7 @@ -8230,6 +8250,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 node-fetch: ^2.6.7 @@ -8251,6 +8272,7 @@ __metadata: "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^" "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 node-fetch: ^2.6.7 @@ -8270,6 +8292,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 git-url-parse: ^14.0.0 msw: ^1.0.0 @@ -8291,6 +8314,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^11.0.0 @@ -8313,6 +8337,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" msw: ^1.0.0 node-fetch: ^2.6.7 yaml: ^2.0.0 @@ -8331,6 +8356,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" msw: ^1.0.0 node-fetch: ^2.6.7 yaml: ^2.0.0 @@ -8350,6 +8376,7 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" "@octokit/webhooks": ^10.0.0 "@types/libsodium-wrappers": ^0.7.10 @@ -8377,6 +8404,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@gitbeaker/core": ^35.8.0 "@gitbeaker/node": ^35.8.0 "@gitbeaker/rest": ^39.25.0 @@ -8399,6 +8427,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^11.0.0 @@ -8419,6 +8448,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" msw: ^2.0.0 yaml: ^2.3.3 @@ -8433,6 +8463,7 @@ __metadata: "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" winston: ^3.2.1 yeoman-environment: ^3.9.1 @@ -8468,6 +8499,7 @@ __metadata: "@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" "@types/express": ^4.17.6 "@types/fs-extra": ^11.0.0 @@ -8492,7 +8524,7 @@ __metadata: prom-client: ^15.0.0 strip-ansi: ^7.1.0 supertest: ^6.1.3 - uuid: ^8.2.0 + uuid: ^9.0.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 yaml: ^2.0.0 @@ -8512,6 +8544,22 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-scaffolder-node-test-utils@workspace:^, @backstage/plugin-scaffolder-node-test-utils@workspace:plugins/scaffolder-node-test-utils": + version: 0.0.0-use.local + resolution: "@backstage/plugin-scaffolder-node-test-utils@workspace:plugins/scaffolder-node-test-utils" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-scaffolder-common": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/types": "workspace:^" + "@testing-library/jest-dom": ^6.0.0 + "@types/react": "*" + winston: ^3.2.1 + languageName: unknown + linkType: soft + "@backstage/plugin-scaffolder-node@workspace:^, @backstage/plugin-scaffolder-node@workspace:plugins/scaffolder-node": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-node@workspace:plugins/scaffolder-node" @@ -8696,7 +8744,7 @@ __metadata: aws4: ^1.12.0 elastic-builder: ^2.16.0 lodash: ^4.17.21 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -8733,7 +8781,7 @@ __metadata: "@backstage/plugin-search-common": "workspace:^" knex: ^3.0.0 lodash: ^4.17.21 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -8800,7 +8848,7 @@ __metadata: lodash: ^4.17.21 lunr: ^2.3.9 ndjson: ^2.0.0 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -8963,7 +9011,7 @@ __metadata: "@types/zen-observable": ^0.8.2 react-hook-form: ^7.12.2 react-use: ^17.2.4 - uuid: ^8.3.2 + uuid: ^9.0.0 zen-observable: ^0.10.0 peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 @@ -8992,7 +9040,7 @@ __metadata: msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 ws: ^8.14.2 yn: ^4.0.0 @@ -9012,7 +9060,7 @@ __metadata: "@backstage/types": "workspace:^" "@types/express": ^4.17.21 express: ^4.17.1 - uuid: ^8.0.0 + uuid: ^9.0.0 ws: ^8.14.2 languageName: unknown linkType: soft @@ -9055,7 +9103,7 @@ __metadata: jest-websocket-mock: ^2.5.0 msw: ^1.0.0 react-use: ^17.2.4 - uuid: ^8.0.0 + uuid: ^9.0.0 peerDependencies: react: ^16.13.1 || ^17.0.0 languageName: unknown @@ -9278,7 +9326,7 @@ __metadata: luxon: ^3.0.0 semver: ^7.5.3 supertest: ^6.1.3 - uuid: ^8.3.2 + uuid: ^9.0.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 yn: ^4.0.0 @@ -11164,10 +11212,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.56.0": - version: 8.56.0 - resolution: "@eslint/js@npm:8.56.0" - checksum: 5804130574ef810207bdf321c265437814e7a26f4e6fac9b496de3206afd52f533e09ec002a3be06cd9adcc9da63e727f1883938e663c4e4751c007d5b58e539 +"@eslint/js@npm:8.57.0": + version: 8.57.0 + resolution: "@eslint/js@npm:8.57.0" + checksum: 315dc65b0e9893e2bff139bddace7ea601ad77ed47b4550e73da8c9c2d2766c7a575c3cddf17ef85b8fd6a36ff34f91729d0dcca56e73ca887c10df91a41b0bb languageName: node linkType: hard @@ -11829,14 +11877,14 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.13": - version: 0.11.13 - resolution: "@humanwhocodes/config-array@npm:0.11.13" +"@humanwhocodes/config-array@npm:^0.11.14": + version: 0.11.14 + resolution: "@humanwhocodes/config-array@npm:0.11.14" dependencies: - "@humanwhocodes/object-schema": ^2.0.1 - debug: ^4.1.1 + "@humanwhocodes/object-schema": ^2.0.2 + debug: ^4.3.1 minimatch: ^3.0.5 - checksum: f8ea57b0d7ed7f2d64cd3944654976829d9da91c04d9c860e18804729a33f7681f78166ef4c761850b8c324d362f7d53f14c5c44907a6b38b32c703ff85e4805 + checksum: 861ccce9eaea5de19546653bccf75bf09fe878bc39c3aab00aeee2d2a0e654516adad38dd1098aab5e3af0145bbcbf3f309bdf4d964f8dab9dcd5834ae4c02f2 languageName: node linkType: hard @@ -11847,10 +11895,10 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^2.0.1": - version: 2.0.1 - resolution: "@humanwhocodes/object-schema@npm:2.0.1" - checksum: 24929487b1ed48795d2f08346a0116cc5ee4634848bce64161fb947109352c562310fd159fc64dda0e8b853307f5794605191a9547f7341158559ca3c8262a45 +"@humanwhocodes/object-schema@npm:^2.0.2": + version: 2.0.2 + resolution: "@humanwhocodes/object-schema@npm:2.0.2" + checksum: 2fc11503361b5fb4f14714c700c02a3f4c7c93e9acd6b87a29f62c522d90470f364d6161b03d1cc618b979f2ae02aed1106fd29d302695d8927e2fc8165ba8ee languageName: node linkType: hard @@ -11926,11 +11974,11 @@ __metadata: "@backstage/plugin-auth-node": "workspace:^" "@types/express": ^4.17.6 "@types/supertest": ^2.0.8 - "@types/uuid": ^8.0.0 + "@types/uuid": ^9.0.0 express: ^4.17.1 express-promise-router: ^4.1.0 supertest: ^6.1.6 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -15968,158 +16016,158 @@ __metadata: languageName: node linkType: hard -"@smithy/config-resolver@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/config-resolver@npm:2.1.1" +"@smithy/config-resolver@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/config-resolver@npm:2.1.2" dependencies: - "@smithy/node-config-provider": ^2.2.1 - "@smithy/types": ^2.9.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/types": ^2.10.0 "@smithy/util-config-provider": ^2.2.1 - "@smithy/util-middleware": ^2.1.1 + "@smithy/util-middleware": ^2.1.2 tslib: ^2.5.0 - checksum: 18c8af60cbc528887a82dc0eabaf0b398d868511dc6b10fa01f41c77ea9c2679ab2137feaee51aa9060dbc5c46fc33325a659f4bd54549c203f64e15dbacbc0a + checksum: 20ac9423e416bbb486d1bca247d7a37a2cbffe30c2e292b15c25e411c6cc5af438362ba2aa8e2218e93ef10c7d7fa04873646c4dd82bbcb4df6203efd1f1d3c9 languageName: node linkType: hard -"@smithy/core@npm:^1.3.2": - version: 1.3.2 - resolution: "@smithy/core@npm:1.3.2" +"@smithy/core@npm:^1.3.3": + version: 1.3.3 + resolution: "@smithy/core@npm:1.3.3" dependencies: - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/util-middleware": ^2.1.1 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/util-middleware": ^2.1.2 tslib: ^2.5.0 - checksum: 5c716b170aa8fb6485b7c98d2d59c44a7333566345727472fb9fabbe86473b33f090fa7a3e08de6ca10829a048c5f20bd238da7da471214789171c7e0a4460a9 + checksum: bb8a79f51517049064f1b5fb2233b9410d8c6dfeafa2ecda31b9d5326f05ffa4773c38ec72d90b380366e8a9b4c21b93231aaadce262c0d87989904950358cd3 languageName: node linkType: hard -"@smithy/credential-provider-imds@npm:^2.2.1": - version: 2.2.1 - resolution: "@smithy/credential-provider-imds@npm:2.2.1" +"@smithy/credential-provider-imds@npm:^2.2.1, @smithy/credential-provider-imds@npm:^2.2.2": + version: 2.2.2 + resolution: "@smithy/credential-provider-imds@npm:2.2.2" dependencies: - "@smithy/node-config-provider": ^2.2.1 - "@smithy/property-provider": ^2.1.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/property-provider": ^2.1.2 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 tslib: ^2.5.0 - checksum: a4e693719384440718728772ea2126be133bbc83fa7bfcefd236942ccb28d1390f1b32fe3262bf330ba4c8e600d01ac73a57110eb42462ec1eb6bbd51e2676a6 + checksum: 85cc9a6e2c52a8f47c0db3dd11c31ff550e5ddd0f6d1917169e22fbf242bb5a5c05bc426da5d04bce79cf0633d0c645e83ca02d42e243f3b40c2ace07aeb1b56 languageName: node linkType: hard -"@smithy/eventstream-codec@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/eventstream-codec@npm:2.1.1" +"@smithy/eventstream-codec@npm:^2.1.1, @smithy/eventstream-codec@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/eventstream-codec@npm:2.1.2" dependencies: "@aws-crypto/crc32": 3.0.0 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 "@smithy/util-hex-encoding": ^2.1.1 tslib: ^2.5.0 - checksum: 7e59028a69e669d1ca1a0fef788f9892a427fad32f33ded731cbfa3bde0163acbc1e7d207e0ce3eae2d3b53f48dce7a99ded092122cdf78e4f392cffd762bfe3 + checksum: ea455826916906a480c7abc517ceb043b578a95994842f802f71e409c45a5575a4f337c376c0c371055371bebafb81119a044cdb4c7de057304e6b1c0527d1d9 languageName: node linkType: hard -"@smithy/eventstream-serde-browser@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/eventstream-serde-browser@npm:2.1.1" +"@smithy/eventstream-serde-browser@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/eventstream-serde-browser@npm:2.1.2" dependencies: - "@smithy/eventstream-serde-universal": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/eventstream-serde-universal": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c909b620de25e9779653742012c665df8c76bf5193bb79054ef302bc3c08b0fa5620884a5965a3a6ebbb4f059da1b05221662a7a652aa979f4830f26c534be60 + checksum: 855118cd6ffc99a05d4950a01af23f727599e7fb127323d508b935617005e3d0e45cf1025fe7ba5079d9def82c73dc8d3e703c36f4b71288fd945c1433b41777 languageName: node linkType: hard -"@smithy/eventstream-serde-config-resolver@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/eventstream-serde-config-resolver@npm:2.1.1" +"@smithy/eventstream-serde-config-resolver@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/eventstream-serde-config-resolver@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 14d4d1c638be460290eb05dec3a700d742f8ce77814c1c235fbd7cf248941a387595f1cd684b9acfc3e081a8d9e6dc2810f10c894b3e08f16f0c3adb130cb736 + checksum: 397d91a492948bced849c5a90422ff6a2d49d4d300ec9fff09eea7256931402966d0285e8fd33a2df3745c10ff5dc88bbfafbf0ce53f0858371d02e7057fd033 languageName: node linkType: hard -"@smithy/eventstream-serde-node@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/eventstream-serde-node@npm:2.1.1" +"@smithy/eventstream-serde-node@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/eventstream-serde-node@npm:2.1.2" dependencies: - "@smithy/eventstream-serde-universal": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/eventstream-serde-universal": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 4be3dd11854d66310273bae07faafd4ca872158be8d3ef7bdc1dec55a175e983975750ebdaf762e74daf80495e379bd2791971a50899076865759a75b2634d73 + checksum: 56a65908d8ac07fd72dfa06a4709972c997a10696c0850de398804590fcc33144afbe1aa70b80ce7d98f6003dffa2b0f5d29228586fb2cb0f076b3d4c03ed20a languageName: node linkType: hard -"@smithy/eventstream-serde-universal@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/eventstream-serde-universal@npm:2.1.1" +"@smithy/eventstream-serde-universal@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/eventstream-serde-universal@npm:2.1.2" dependencies: - "@smithy/eventstream-codec": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/eventstream-codec": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 99c7cf5b869f8e6323e976335a3238b77d3b1c32005fc78093d448981883294e4d59bcbd419e88d6a53c76aab01c27bc9af63a5dfed9451d2302eaf6ccddbd64 + checksum: 693be21ef300c26f638fd7f9b9b36652aff319d7316893a661f32a5f1f29369bc216eb6f1d9c80d5e42473ccdd83e332163a8c9fce012c08df4305a52dea09c7 languageName: node linkType: hard -"@smithy/fetch-http-handler@npm:^2.4.1": - version: 2.4.1 - resolution: "@smithy/fetch-http-handler@npm:2.4.1" +"@smithy/fetch-http-handler@npm:^2.4.2": + version: 2.4.2 + resolution: "@smithy/fetch-http-handler@npm:2.4.2" dependencies: - "@smithy/protocol-http": ^3.1.1 - "@smithy/querystring-builder": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/protocol-http": ^3.2.0 + "@smithy/querystring-builder": ^2.1.2 + "@smithy/types": ^2.10.0 "@smithy/util-base64": ^2.1.1 tslib: ^2.5.0 - checksum: c23701d45bca6842b5206939ccd587e3482ace9f656ae3dca92ff0bad3fefb846cc33683dff41a19186f2a5662ca6cd66c8aefda4664b7dfd95f9a616055a1c1 + checksum: 7d87d5c6674623250972ac673a3317eeaeeba0647d8095c92e63ec9a002e96bb56dd7aa75172e474e226a4971f2abbd2506025cb1bf131d3b45698dbff27220d languageName: node linkType: hard -"@smithy/hash-blob-browser@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/hash-blob-browser@npm:2.1.1" +"@smithy/hash-blob-browser@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/hash-blob-browser@npm:2.1.2" dependencies: "@smithy/chunked-blob-reader": ^2.1.1 "@smithy/chunked-blob-reader-native": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: f4dc57c11ef32ddea0e7094d2c230aa274f1e410d84c789d8f5e2ed8a090da8675ca76da9605d297285324107ea8106af1c2aab2859bd62d6e9a8db415eb8e55 + checksum: e8d9fcedcfd03d03603753d6e4aafd7ad7c26e9ed629bf54a8dbb2ecd14b9e29cd267209453479eab698cb19329bfc80632823b40f49cb6d8b949aa4cb6db7c4 languageName: node linkType: hard -"@smithy/hash-node@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/hash-node@npm:2.1.1" +"@smithy/hash-node@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/hash-node@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 "@smithy/util-buffer-from": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 5d5aae69b94dcb8abaf9f6a5b53ee320c9e126445c4540fcf2169e8ea7ebd953acff7fd77ba514614f6ebbb0baf412e878eebcc3427a5b9b6f8ee39abbc59230 + checksum: 2f4fe6120a177afbc540c0ba904a3285a0b81de576a57bb28dbee94186635ab585034e7f48eeff0950d3a6442f5fd932cd66c366199c42f4314a540d02261eba languageName: node linkType: hard -"@smithy/hash-stream-node@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/hash-stream-node@npm:2.1.1" +"@smithy/hash-stream-node@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/hash-stream-node@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: da3c4ba14c648ee0d2fe7d3298d601150ee0ce5ac0c7d9f54a88148b5f67b03513b41560f76f5f109f11196547b4dc4f26e314774794596d7e3ee1103a9906a8 + checksum: 5a1a4d4fff29a4ba048900c606c98baeac0daabbaeba77c0c5b603d5896eff7f1eba2b012d41344638ce3d7fe10cb7d8d88551d34f1c637e807adb61efe3f43e languageName: node linkType: hard -"@smithy/invalid-dependency@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/invalid-dependency@npm:2.1.1" +"@smithy/invalid-dependency@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/invalid-dependency@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: f95ecd9acd337a408b6608a3f451b24a61e26149878f61fc7855c724888f0d28abf0b798d16990dadb7eafc8027098f934c0cd44e75d01d31617bd1fbfd93935 + checksum: 5f5ce3d408c67c3e8b80c7dbc3504662c437ded360c360f072ed731c0bae6b57386ba37dc059883dfe7d23711680a5119a68e05b3ea6ccc7fc124cf0e6f90024 languageName: node linkType: hard @@ -16132,93 +16180,93 @@ __metadata: languageName: node linkType: hard -"@smithy/md5-js@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/md5-js@npm:2.1.1" +"@smithy/md5-js@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/md5-js@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: d15bc426a46d80d450b555a5ccd3d5a6bf37190f4b9ccb705852cd53ce61e4fe6fb08a569b87303ee787da57023f2b75f0e7893644af16c89e9aaf513f8afff3 + checksum: c6e4bdb779e9af5146e502d1e0d757e09a991b70e39fdb089efbcb8e511761942330034b215b66814c3627b0b0bb7bd028d0f26e8558b0ec5b2ea07c46b5f4e5 languageName: node linkType: hard -"@smithy/middleware-content-length@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/middleware-content-length@npm:2.1.1" +"@smithy/middleware-content-length@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/middleware-content-length@npm:2.1.2" dependencies: - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: cb0ea801f72a1a01f5956b3526df930fc19762b07d43a3871ff29815f621603410753d37710d72675d9761b93da32a38cfd5195582de8b6a47e299b1f073be25 + checksum: ddea93b236e5f916da8e1574317967d5aa449e78b0c7153c60c821d117f1648b00effad5301095919de9225810cd8f90f8ee76e7b95c346fc616d8598ad54447 languageName: node linkType: hard -"@smithy/middleware-endpoint@npm:^2.4.1": - version: 2.4.1 - resolution: "@smithy/middleware-endpoint@npm:2.4.1" +"@smithy/middleware-endpoint@npm:^2.4.2": + version: 2.4.2 + resolution: "@smithy/middleware-endpoint@npm:2.4.2" dependencies: - "@smithy/middleware-serde": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 - "@smithy/util-middleware": ^2.1.1 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/shared-ini-file-loader": ^2.3.2 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 + "@smithy/util-middleware": ^2.1.2 tslib: ^2.5.0 - checksum: 685f74c76cba205bdb20ad7bda449b73e498ae2e9074a026d48b38c7b4456d8a0cfb4fdb48625b65f93f3a75e92eaf7951db28f8e9f44e50ce18fd59a7b325af + checksum: 3e989123fc608c9a32abf30c4033718b3da665a63bd84e8e2869d4aecb0545d461506e75f82b91bbc35a07915ddadf1432643e4c937c11447847a9a45b3de9fa languageName: node linkType: hard -"@smithy/middleware-retry@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/middleware-retry@npm:2.1.1" +"@smithy/middleware-retry@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/middleware-retry@npm:2.1.2" dependencies: - "@smithy/node-config-provider": ^2.2.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/service-error-classification": ^2.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/protocol-http": ^3.2.0 + "@smithy/service-error-classification": ^2.1.2 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 tslib: ^2.5.0 uuid: ^8.3.2 - checksum: a4bc59d2ff8f65367aeb93391a2aafc7caf8031d8b2dfb32ee35748cdc46e06d5182c37bee90d7a107e890959bd40e6a7f4041bc1b0b36a99d14919b1cc78812 + checksum: ec04fd0c362070529ecd52f2dadd6fd4d638a4e35c62a67309791f1a288550ee8fcd6406ec27fea78d3c89b08a9c6d29ce63bf7cb5a1b0269f86a759b233dac4 languageName: node linkType: hard -"@smithy/middleware-serde@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/middleware-serde@npm:2.1.1" +"@smithy/middleware-serde@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/middleware-serde@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: ed77b80ac6b68640ee4bf8310bc4d9f5aa13de2741333f6f03a4983e897fa66e0de057d178e78d9ba095d5686d3e4531437c9dd2583366efe948bd75b2aa8581 + checksum: 4f5bd5ee173cf20cd1c12838b0802f96df5a14c3bdab2d50a6009965c128596863c095633958f0c74d82bca3cc9343f8a4c659c033b9a75e614e3a85e34e0665 languageName: node linkType: hard -"@smithy/middleware-stack@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/middleware-stack@npm:2.1.1" +"@smithy/middleware-stack@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/middleware-stack@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 0d7c1051c96fcf19f7d5e96bc59484ce13df4e570c1da3eda74d23a7911b41eb61d6c378aad0aa21f7e9c72934148bdf39f9767c57abd4845aa4417a84e3f6e4 + checksum: f93dda40f08051a6391e213cc3b90f30ebb31399b000bba882cbf37f942786030822e10ccfe576005361ef6341b571460ce791fbdc7dad971c99a33a92e400dd languageName: node linkType: hard -"@smithy/node-config-provider@npm:^2.2.1": - version: 2.2.1 - resolution: "@smithy/node-config-provider@npm:2.2.1" +"@smithy/node-config-provider@npm:^2.2.2": + version: 2.2.2 + resolution: "@smithy/node-config-provider@npm:2.2.2" dependencies: - "@smithy/property-provider": ^2.1.1 - "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/property-provider": ^2.1.2 + "@smithy/shared-ini-file-loader": ^2.3.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 62ed3124d888a10cac633a250fbe12d6c5b8aa75ea691889abebce227cbaf155f3db00fa6beb453fbd6147e667e70819d043da1750980669451281a28eafd285 + checksum: 666d80d893985e6af5aa88a2f3ed07bd68c6873805974331fd148ec5a5d331e5116e2dca8656ef57c60e22cec03aee0717061a03ce703a691765bf94d809f2eb languageName: node linkType: hard -"@smithy/node-http-handler@npm:^2.1.7, @smithy/node-http-handler@npm:^2.3.1": +"@smithy/node-http-handler@npm:^2.1.7, @smithy/node-http-handler@npm:^2.4.0": version: 2.4.0 resolution: "@smithy/node-http-handler@npm:2.4.0" dependencies: @@ -16231,17 +16279,17 @@ __metadata: languageName: node linkType: hard -"@smithy/property-provider@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/property-provider@npm:2.1.1" +"@smithy/property-provider@npm:^2.1.1, @smithy/property-provider@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/property-provider@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: e87d70c4efe07e830cfb2094b046af89175b87b13259fba37641aa7bfc2ab0c7bf2397797ac48b92e1feb11bf6129b82b350519172093efd7ac4d3a4a98bbe2f + checksum: df2b72912ede1843a75220a458e3ff8ec70e5544c990c0915e615507380cea4c28bb39b425b7ee600f5c3c90d53b5c82ceaf14d641348f465c14640c556ac9bd languageName: node linkType: hard -"@smithy/protocol-http@npm:^3.1.1, @smithy/protocol-http@npm:^3.2.0": +"@smithy/protocol-http@npm:^3.2.0": version: 3.2.0 resolution: "@smithy/protocol-http@npm:3.2.0" dependencies: @@ -16251,7 +16299,7 @@ __metadata: languageName: node linkType: hard -"@smithy/querystring-builder@npm:^2.1.1, @smithy/querystring-builder@npm:^2.1.2": +"@smithy/querystring-builder@npm:^2.1.2": version: 2.1.2 resolution: "@smithy/querystring-builder@npm:2.1.2" dependencies: @@ -16262,32 +16310,32 @@ __metadata: languageName: node linkType: hard -"@smithy/querystring-parser@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/querystring-parser@npm:2.1.1" +"@smithy/querystring-parser@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/querystring-parser@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: bfac40793b0e42f4e25137db4e7d866debfa32557359cc41e02a23174a6fd8e0132f098cef5669a3ddf5211e477c9c97d4aa9039b35c7b4a29f2207236da236e + checksum: 02a1e3a31b37b59adb162d3a2cb084852c2ea01dec948b0669939e77241b05fd7f5b00734418b925248f0b6c164bc483e897438cb2b1a750829f6b4aab0fa8d1 languageName: node linkType: hard -"@smithy/service-error-classification@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/service-error-classification@npm:2.1.1" +"@smithy/service-error-classification@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/service-error-classification@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 - checksum: 59a5e3cb0fb42d70fc2d85814124abbff60e28cc9aa45d87fde3370e25943abaf4b6baf62cc40e496c3687e9fa9161156a055ad29a4f7ce8dd7d937bbf49f9a7 + "@smithy/types": ^2.10.0 + checksum: 8a26f553fd2a823179b701f87c0952e58580c9297166e608a036c7b313d5cc1399bc8b4b056b038003aedc0145643c6c6323e7f683b1a2d1140d9a7982e6bf7c languageName: node linkType: hard -"@smithy/shared-ini-file-loader@npm:^2.3.1": - version: 2.3.1 - resolution: "@smithy/shared-ini-file-loader@npm:2.3.1" +"@smithy/shared-ini-file-loader@npm:^2.3.1, @smithy/shared-ini-file-loader@npm:^2.3.2": + version: 2.3.2 + resolution: "@smithy/shared-ini-file-loader@npm:2.3.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 89b0dfb65faab917fcb4a6a8f34a85d668a759ccbfd6c4dc3d6311e59a8f1b78baab1d97402c333d2207da810cb00de9d5b4379f114bde82135f9aa0d0069cab + checksum: 6db5ac83a76a15f3bf49496747ef4d20343e87a4de35b87892c5ac5c69a7046ffe7276230a4e9cbc075183d8b0584f1530a878f58324279cf936103c578aa70a languageName: node linkType: hard @@ -16307,17 +16355,17 @@ __metadata: languageName: node linkType: hard -"@smithy/smithy-client@npm:^2.3.1": - version: 2.3.1 - resolution: "@smithy/smithy-client@npm:2.3.1" +"@smithy/smithy-client@npm:^2.4.0": + version: 2.4.0 + resolution: "@smithy/smithy-client@npm:2.4.0" dependencies: - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 - "@smithy/util-stream": ^2.1.1 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 + "@smithy/util-stream": ^2.1.2 tslib: ^2.5.0 - checksum: 9b13c361528b3120b1a1db17cd60521d04c72f664c2709be20934cea12756117441d2a33d0464ff3099be11ccb12946c62ece1126b9532eb8f6243a35d6fd171 + checksum: af17a6334e0b19323145482d829b664fcc3102cfbea9682753b9bc328840b9bc1968cd3cf64677cdc23c824194061f8fdf3a905d6f71431547a4ec413d557f92 languageName: node linkType: hard @@ -16339,14 +16387,14 @@ __metadata: languageName: node linkType: hard -"@smithy/url-parser@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/url-parser@npm:2.1.1" +"@smithy/url-parser@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/url-parser@npm:2.1.2" dependencies: - "@smithy/querystring-parser": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/querystring-parser": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 5c939f3ff9c53a0b7a0c5a1ac7641f229598d2bf9499e1abf4d33c1c1cd13bd5f7fcfffd00c366ca9f8092d28979a4a958b80f9bbc91e817e4d1940451e93489 + checksum: 83aca5a6474e85d835958caed5b486d5e6438682e6c17a5817ebda48ec9936c68b7b39c35a050f7eb4bd3902e83d8008b26d9fc5df7a168502d6ed93848005c6 languageName: node linkType: hard @@ -16397,42 +16445,42 @@ __metadata: languageName: node linkType: hard -"@smithy/util-defaults-mode-browser@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/util-defaults-mode-browser@npm:2.1.1" +"@smithy/util-defaults-mode-browser@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-defaults-mode-browser@npm:2.1.2" dependencies: - "@smithy/property-provider": ^2.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/property-provider": ^2.1.2 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 bowser: ^2.11.0 tslib: ^2.5.0 - checksum: 5d3b11be1768410e24ad9829dc70bed9b50419f85a8ca934c6296e21e278d87f665cfdb603241ef749f80d154a2c4be26cd29338daecc625d31b30af8bd9c139 + checksum: bc0621f1d5ca46830a4b525def4b829e23336707af28e305758de7f97170024f387a9f3d73e2e29b033ea4963466373df644a304a87291c980159b31cf5ffbcf languageName: node linkType: hard -"@smithy/util-defaults-mode-node@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/util-defaults-mode-node@npm:2.2.0" +"@smithy/util-defaults-mode-node@npm:^2.2.1": + version: 2.2.1 + resolution: "@smithy/util-defaults-mode-node@npm:2.2.1" dependencies: - "@smithy/config-resolver": ^2.1.1 - "@smithy/credential-provider-imds": ^2.2.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/property-provider": ^2.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/config-resolver": ^2.1.2 + "@smithy/credential-provider-imds": ^2.2.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/property-provider": ^2.1.2 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c4a69b73bc46c3bb5ff4149b80bdfa79f4c25b82253d9c7168c9920066e12830e1bea324dce09414b09791fd0379bdc05c39117155d5b37a229d226962a95d5f + checksum: 672c13329e37d61170fb6c997b416fe9216efb6308b2e15560f5a96089bd839018d4705fb541aed0717dd1e1574e3dfc835f37a9608679713ee679d1d7c17642 languageName: node linkType: hard -"@smithy/util-endpoints@npm:^1.1.1": - version: 1.1.1 - resolution: "@smithy/util-endpoints@npm:1.1.1" +"@smithy/util-endpoints@npm:^1.1.2": + version: 1.1.2 + resolution: "@smithy/util-endpoints@npm:1.1.2" dependencies: - "@smithy/node-config-provider": ^2.2.1 - "@smithy/types": ^2.9.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 40619bf739c1fc959486946cb49319f34c9c4c5c19f46cdefc7ff8e7331b84f6ad7a4aeb8a0268f6d77d266ff5ec9df8d2244094dd79ae469983e9c07e43766a + checksum: 261f383e64116f767cc8e304a647c47261fee8425c43e517511e1bf8ec5e72652b8a12c65259ca4557f3fba477662606253d4cf89ccab5af184a05c59d2d735f languageName: node linkType: hard @@ -16445,40 +16493,40 @@ __metadata: languageName: node linkType: hard -"@smithy/util-middleware@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/util-middleware@npm:2.1.1" +"@smithy/util-middleware@npm:^2.1.1, @smithy/util-middleware@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-middleware@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 404bb944202df70ba0ff8bb6ea105ead0a6b365d5ef7bfafbfc919df228823563818f0ee36f0f1e20462200da2fb8c8961e20b237e4e1bd9f77c38dd701f39ab + checksum: 8a05c05ba1358515aa6881189cd4a6a57701e8cd9e036c8d7219662fd12bce1695a4970c247314e0020f13bb506a558545ab5c519373647be89d79af08d5bcdf languageName: node linkType: hard -"@smithy/util-retry@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/util-retry@npm:2.1.1" +"@smithy/util-retry@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-retry@npm:2.1.2" dependencies: - "@smithy/service-error-classification": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/service-error-classification": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 1747c75f55a208f16104483cd76ec45200dedaa924868e84d4882b88f8b4a8d3a4422834359fd9bfba242e0e96a474349ac0a6f5d804fb15b15e8b639b6d2ad0 + checksum: 3be4b984b0f1daa54948fe158568a41003f725464ef32f0ccf32e02b566545364bd6f89a380a218397742edd1ad1d214906fab27debce531c137bfceca0c9c6d languageName: node linkType: hard -"@smithy/util-stream@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/util-stream@npm:2.1.1" +"@smithy/util-stream@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-stream@npm:2.1.2" dependencies: - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/types": ^2.10.0 "@smithy/util-base64": ^2.1.1 "@smithy/util-buffer-from": ^2.1.1 "@smithy/util-hex-encoding": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 3a060226b8a506e722d0d8c1c4b7a2989241f7946c8acc892a8a70d92d9952cc8619b14bf686c9c822115d99159c6c16534bad2d72ecc2809a56f865224e82a6 + checksum: 8b95535323fcf3ce86cbb070791405afd4de1513a38fef209bfc4d5b2ed91ae16ae40393dd8a5f8b127194ec023cc264808f87beef0678219f56b2bcb580eb65 languageName: node linkType: hard @@ -16501,14 +16549,14 @@ __metadata: languageName: node linkType: hard -"@smithy/util-waiter@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/util-waiter@npm:2.1.1" +"@smithy/util-waiter@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-waiter@npm:2.1.2" dependencies: - "@smithy/abort-controller": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/abort-controller": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 52d9c82bb9684b6b11eeb2814fa1454514cb90aeeb87bfdf7c458613c13d18189712585486859c975824d08f2d1e3c817dd7e51c400531aaa479af8a06ea0bff + checksum: 089e777701ff2d6d8910f843c73de0d504221064401a02e56f104b5a50c66abfe8c8fa41c9e9ec62c8b6088f234cd995afccd9e2220fa7b7c8fcbf67fe343062 languageName: node linkType: hard @@ -17943,15 +17991,6 @@ __metadata: languageName: node linkType: hard -"@types/archiver@npm:^5.3.1": - version: 5.3.4 - resolution: "@types/archiver@npm:5.3.4" - dependencies: - "@types/readdir-glob": "*" - checksum: 4ef27b99091ada9b8f13017d5b9e6d42a439e35a7858b30e040c408e081d98d8db6307b0762500288b5da38cab9823c4756b6abae1fdd2658d42bfb09eb7c5fb - languageName: node - linkType: hard - "@types/archiver@npm:^6.0.0": version: 6.0.2 resolution: "@types/archiver@npm:6.0.2" @@ -18408,13 +18447,13 @@ __metadata: languageName: node linkType: hard -"@types/dockerode@npm:^3.3.0, @types/dockerode@npm:^3.3.8": - version: 3.3.23 - resolution: "@types/dockerode@npm:3.3.23" +"@types/dockerode@npm:^3.3.0, @types/dockerode@npm:^3.3.21": + version: 3.3.24 + resolution: "@types/dockerode@npm:3.3.24" dependencies: "@types/docker-modem": "*" "@types/node": "*" - checksum: 065e9ae43f13641e0df149335914d10af95e559002efe5a5de8e56df9a21b4309b18dbc04fd4c59a4e893c4fedc7df892db28e9d25457bce0c4fd33d21a67834 + checksum: 00329ba9225f5b57bfc0ba8c4dddb17100ebe13c5192fe8a14fce59eec456d258e814b6c78df26d7d7bb878fc38455f559f278490c9c8efad8f708335643b40e languageName: node linkType: hard @@ -18867,7 +18906,7 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:^4.14.151, @types/lodash@npm:^4.14.173, @types/lodash@npm:^4.14.175": +"@types/lodash@npm:^4.14.151, @types/lodash@npm:^4.14.173": version: 4.14.202 resolution: "@types/lodash@npm:4.14.202" checksum: a91acf3564a568c6f199912f3eb2c76c99c5a0d7e219394294213b3f2d54f672619f0fde4da22b29dc5d4c31457cd799acc2e5cb6bd90f9af04a1578483b6ff7 @@ -18895,13 +18934,6 @@ __metadata: languageName: node linkType: hard -"@types/marked@npm:^5.0.0": - version: 5.0.2 - resolution: "@types/marked@npm:5.0.2" - checksum: 2875618970bd5aaba472e313c799bbe241fe9e31d1e79782841a0cc04e08ab2a98653166f1fb99bf8bcf140d3878c3ab960a12aa8f0fb949d8277e8a01d3411b - languageName: node - linkType: hard - "@types/mdast@npm:^3.0.0, @types/mdast@npm:^3.0.3": version: 3.0.10 resolution: "@types/mdast@npm:3.0.10" @@ -19188,13 +19220,13 @@ __metadata: linkType: hard "@types/pg@npm:^8.6.6": - version: 8.11.0 - resolution: "@types/pg@npm:8.11.0" + version: 8.11.1 + resolution: "@types/pg@npm:8.11.1" dependencies: "@types/node": "*" pg-protocol: "*" pg-types: ^4.0.1 - checksum: 8ae18abce86a012afdd68b2fb85a9fd0e9529f2dae8ca64311a4804fc8423441d605df51f547170efa4584c6ee9f919b4f5f731d5a6221386c5d04560de4334c + checksum: 3d8672800cc96ffeec934c0f7c652d699a1c5a891804e89b6783325b04c496c08ce32237a93da64e3a83540f0f2c3d20d344313716e6f1ea7f335da38a4fd241 languageName: node linkType: hard @@ -19377,13 +19409,13 @@ __metadata: linkType: hard "@types/react@npm:^18": - version: 18.2.57 - resolution: "@types/react@npm:18.2.57" + version: 18.2.58 + resolution: "@types/react@npm:18.2.58" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 01e7a3424162468428f3b28acec5e5c6cd1e26775ff605d0f46c883dea2d835924873d36b9ea0b75e40c9593aa78ca56a8ccde66bd58dbf6ecb0dd95af28609d + checksum: 42551e30c8a54161a11b2ecd11406782ddba4472a4471d45034c551295263d56f06234f283526d0c0420352ce9ce9675b2a6c65db7a287d9613643d3ceaaf1f0 languageName: node linkType: hard @@ -19500,9 +19532,9 @@ __metadata: linkType: hard "@types/semver@npm:^7.3.12, @types/semver@npm:^7.3.8, @types/semver@npm:^7.5.0": - version: 7.5.7 - resolution: "@types/semver@npm:7.5.7" - checksum: 5af9b13e3d74d86d4b618f6506ccbded801fb35dbc28608cd5a7bfb8bcac0021dd35ef305a72a0c2a8def0cff60acd706bfee16a9ed1c39a893d2a175e778ea7 + version: 7.5.8 + resolution: "@types/semver@npm:7.5.8" + checksum: ea6f5276f5b84c55921785a3a27a3cd37afee0111dfe2bcb3e03c31819c197c782598f17f0b150a69d453c9584cd14c4c4d7b9a55d2c5e6cacd4d66fdb3b3663 languageName: node linkType: hard @@ -19770,10 +19802,10 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^8.0.0": - version: 8.3.4 - resolution: "@types/uuid@npm:8.3.4" - checksum: 6f11f3ff70f30210edaa8071422d405e9c1d4e53abbe50fdce365150d3c698fe7bbff65c1e71ae080cbfb8fded860dbb5e174da96fdbbdfcaa3fb3daa474d20f +"@types/uuid@npm:^9.0.0": + version: 9.0.8 + resolution: "@types/uuid@npm:9.0.8" + checksum: b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 languageName: node linkType: hard @@ -20088,9 +20120,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.21.23": - version: 4.21.23 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.21.23" +"@uiw/codemirror-extensions-basic-setup@npm:4.21.24": + version: 4.21.24 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.21.24" dependencies: "@codemirror/autocomplete": ^6.0.0 "@codemirror/commands": ^6.0.0 @@ -20107,19 +20139,19 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: cd17481d9d9a9b620f961a4df6e8208bcabe98652ca6c18366a8688fbcc09d37a030694a70dcc010aaabe02c85c342acf9f80357c6853b57f1081af5b130bd26 + checksum: db42a1651d7d482e1811cd629a3a8a53c3ac09bfabf376dd35c0cbbaf5780f80c873d6da55294245850f7d8e13da92ec5885eea4938c85c957c38c23394e11f9 languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.9.3": - version: 4.21.23 - resolution: "@uiw/react-codemirror@npm:4.21.23" + version: 4.21.24 + resolution: "@uiw/react-codemirror@npm:4.21.24" dependencies: "@babel/runtime": ^7.18.6 "@codemirror/commands": ^6.1.0 "@codemirror/state": ^6.1.1 "@codemirror/theme-one-dark": ^6.0.0 - "@uiw/codemirror-extensions-basic-setup": 4.21.23 + "@uiw/codemirror-extensions-basic-setup": 4.21.24 codemirror: ^6.0.0 peerDependencies: "@babel/runtime": ">=7.11.0" @@ -20129,7 +20161,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 7d0209d947e1e57cf80ef44a097bb3af04d2bab2d6fc57deed91619a0db77e3d4289db8f03fb43906b2e324496d25c872fa99b83834f085c40ad95137ea4459c + checksum: 6adbee6608f3ec0806ff4b2d2e759fd314d3727c805cf58a6b1d524fe4e12b261b38d31374307eeb51aa34f68e113b298efdc13de807e93ca50a357e5b0f3eff languageName: node linkType: hard @@ -21060,7 +21092,7 @@ __metadata: languageName: node linkType: hard -"archiver@npm:^5.3.1": +"archiver@npm:^5.3.2": version: 5.3.2 resolution: "archiver@npm:5.3.2" dependencies: @@ -21432,10 +21464,10 @@ __metadata: languageName: node linkType: hard -"async-lock@npm:^1.1.0": - version: 1.2.4 - resolution: "async-lock@npm:1.2.4" - checksum: 9b8cf65bb9ac7b58ff95539a03b73d51f64d0aea95cde1ebf787859670a8998f0a5258f118db6b54305bf6ed20cf3a2f923f4dd69a58d472fa78cca436c42342 +"async-lock@npm:^1.1.0, async-lock@npm:^1.4.0": + version: 1.4.1 + resolution: "async-lock@npm:1.4.1" + checksum: 29e70cd892932b7c202437786cedc39ff62123cb6941014739bd3cabd6106326416e9e7c21285a5d1dc042cad239a0f7ec9c44658491ee4a615fd36a21c1d10a languageName: node linkType: hard @@ -21901,6 +21933,41 @@ __metadata: languageName: node linkType: hard +"bare-events@npm:^2.0.0, bare-events@npm:^2.2.0": + version: 2.2.0 + resolution: "bare-events@npm:2.2.0" + checksum: b3001d61cbb7e6c91c7e47ed1d5701512f94c68955a88c1fe368ff313ba68f372fd701f422d1604fd6ac6e2237024d99373aa14e43a92696755a1f7ae46a8626 + languageName: node + linkType: hard + +"bare-fs@npm:^2.1.1": + version: 2.2.0 + resolution: "bare-fs@npm:2.2.0" + dependencies: + bare-events: ^2.0.0 + bare-os: ^2.0.0 + bare-path: ^2.0.0 + streamx: ^2.13.0 + checksum: 8832abc6c222bdfc8dcf37253493eefdd153048dd2fd482fe7722d6fea083f9e44574197a47e2b0046057f9fb271078ed799d03663e387ad06d2ab116a64cce4 + languageName: node + linkType: hard + +"bare-os@npm:^2.0.0, bare-os@npm:^2.1.0": + version: 2.2.0 + resolution: "bare-os@npm:2.2.0" + checksum: ed78e2f3ea498e35c7565532ae3aa3b85a7e5e223ab6353de64864823cadff02a2a8b7722e9a6c1a0ff56cb9f21f23ada8e88a085cc0a5d38a7c1bcf65e8f7fd + languageName: node + linkType: hard + +"bare-path@npm:^2.0.0, bare-path@npm:^2.1.0": + version: 2.1.0 + resolution: "bare-path@npm:2.1.0" + dependencies: + bare-os: ^2.1.0 + checksum: 03f260e72bd0ae0df4cd712322a2d3c8c16701ffaa55cf2d517ae62b7f78c64b7ec5bba81ec579367f966472481f5160db282e6663bd0fc8cfb09ebe272d8bba + languageName: node + linkType: hard + "base16@npm:^1.0.0": version: 1.0.0 resolution: "base16@npm:1.0.0" @@ -25478,12 +25545,12 @@ __metadata: languageName: node linkType: hard -"docker-compose@npm:^0.23.17": - version: 0.23.17 - resolution: "docker-compose@npm:0.23.17" +"docker-compose@npm:^0.24.2": + version: 0.24.6 + resolution: "docker-compose@npm:0.24.6" dependencies: - yaml: ^1.10.2 - checksum: c308bf067cabe178d245b3e499119937b1d2a5effdc9fac6227e04be4308a0250ca7bb1471789b3d0492ea2ce83f74e40b7517a9a5cb540a21355a64e4ad5d3c + yaml: ^2.2.2 + checksum: 7926e72d7feb9e7feb9e9d46460e18a61cf759cdb9004d7783b58a815eb22b3fbd6402db903cdb764be289cacb1960ef6d19904a9ee435991d8937b146be590f languageName: node linkType: hard @@ -25511,7 +25578,7 @@ __metadata: languageName: node linkType: hard -"dockerode@npm:^3.3.1": +"dockerode@npm:^3.3.5": version: 3.3.5 resolution: "dockerode@npm:3.3.5" dependencies: @@ -25917,8 +25984,8 @@ __metadata: linkType: hard "elastic-builder@npm:^2.16.0": - version: 2.24.0 - resolution: "elastic-builder@npm:2.24.0" + version: 2.25.0 + resolution: "elastic-builder@npm:2.25.0" dependencies: lodash.has: ^4.5.2 lodash.hasin: ^4.5.2 @@ -25928,7 +25995,7 @@ __metadata: lodash.isobject: ^3.0.2 lodash.isstring: ^4.0.1 lodash.omit: ^4.5.0 - checksum: 4bbfa66a179b78dbd90a0a3ee19cf7fc2105deca195c65d3047cf2ef2e6fa4925049038ed1f28d46a3ce96daa47f2cf8d924ea2e2fc0c585569a5a7acd09f2ed + checksum: 576b1060174cd5b62f5f802f9b947a3481ed0477c9c1ccbfe572f6ef4c3287c7ba0f5101cf6d32a69e1097e36fb95bded4066ef1ba195176f3b6b8c546d66e6d languageName: node linkType: hard @@ -26927,14 +26994,14 @@ __metadata: linkType: hard "eslint@npm:^8.33.0, eslint@npm:^8.6.0": - version: 8.56.0 - resolution: "eslint@npm:8.56.0" + version: 8.57.0 + resolution: "eslint@npm:8.57.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.6.1 "@eslint/eslintrc": ^2.1.4 - "@eslint/js": 8.56.0 - "@humanwhocodes/config-array": ^0.11.13 + "@eslint/js": 8.57.0 + "@humanwhocodes/config-array": ^0.11.14 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 "@ungap/structured-clone": ^1.2.0 @@ -26970,7 +27037,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 883436d1e809b4a25d9eb03d42f584b84c408dbac28b0019f6ea07b5177940bf3cca86208f749a6a1e0039b63e085ee47aca1236c30721e91f0deef5cc5a5136 + checksum: 3a48d7ff85ab420a8447e9810d8087aea5b1df9ef68c9151732b478de698389ee656fd895635b5f2871c89ee5a2652b3f343d11e9db6f8486880374ebc74a2d9 languageName: node linkType: hard @@ -27385,6 +27452,7 @@ __metadata: "@backstage/plugin-app-backend": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" + "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-azure-devops-backend": "workspace:^" "@backstage/plugin-badges-backend": "workspace:^" @@ -28019,15 +28087,6 @@ __metadata: languageName: node linkType: hard -"fd-slicer@npm:~1.1.0": - version: 1.1.0 - resolution: "fd-slicer@npm:1.1.0" - dependencies: - pend: ~1.2.0 - checksum: c8585fd5713f4476eb8261150900d2cb7f6ff2d87f8feb306ccc8a1122efd152f1783bdb2b8dc891395744583436bfd8081d8e63ece0ec8687eeefea394d4ff2 - languageName: node - linkType: hard - "fecha@npm:^4.2.0": version: 4.2.0 resolution: "fecha@npm:4.2.0" @@ -34165,6 +34224,15 @@ __metadata: languageName: node linkType: hard +"marked@npm:^12.0.0": + version: 12.0.0 + resolution: "marked@npm:12.0.0" + bin: + marked: bin/marked.js + checksum: 973e803debf7eca946213cd583792c2968ecab1b6698653604272d5406f9f07375a4865a7a28633a7200c8a4682761f8d866198fb36c10c686e3bda3a9b36fc9 + languageName: node + linkType: hard + "marked@npm:^4.0.14": version: 4.3.0 resolution: "marked@npm:4.3.0" @@ -35570,8 +35638,8 @@ __metadata: linkType: hard "mysql2@npm:^3.0.0": - version: 3.9.1 - resolution: "mysql2@npm:3.9.1" + version: 3.9.2 + resolution: "mysql2@npm:3.9.2" dependencies: denque: ^2.1.0 generate-function: ^2.3.1 @@ -35581,7 +35649,7 @@ __metadata: named-placeholders: ^1.1.3 seq-queue: ^0.0.5 sqlstring: ^2.3.2 - checksum: 067353f8735d3e91654ecc01f562729c87f4fa141e870d524af06c5db98ac3341d91f3130357fead247833f41b1b93d1c6dd6e1f1b98687d28998bd9673b9ce7 + checksum: a236a52659d67812af494bc41d09a2bd906d12755887ba75bbdf271c25ad5668030e0a2e91dc61ba4b3b225fb2b5a6aa07a168e077fcdaf50d6a73930c11de5d languageName: node linkType: hard @@ -35633,13 +35701,6 @@ __metadata: languageName: node linkType: hard -"nanoclone@npm:^0.2.1": - version: 0.2.1 - resolution: "nanoclone@npm:0.2.1" - checksum: 96b2954e22f70561f41e20d69856266c65583c2a441dae108f1dc71b716785d2c8038dac5f1d5e92b117aed3825f526b53139e2e5d6e6db8a77cfa35b3b8bf40 - languageName: node - linkType: hard - "nanoid@npm:^3.3.7": version: 3.3.7 resolution: "nanoid@npm:3.3.7" @@ -35864,7 +35925,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": +"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -37329,7 +37390,7 @@ __metadata: languageName: node linkType: hard -"passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1": +"passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1, passport-oauth2@npm:^1.7.0": version: 1.8.0 resolution: "passport-oauth2@npm:1.8.0" dependencies: @@ -38748,19 +38809,30 @@ __metadata: languageName: node linkType: hard -"properties-reader@npm:^2.2.0": - version: 2.2.0 - resolution: "properties-reader@npm:2.2.0" +"proper-lockfile@npm:^4.1.2": + version: 4.1.2 + resolution: "proper-lockfile@npm:4.1.2" dependencies: - mkdirp: ^1.0.4 - checksum: a5c5684b1e16633cb695f4fef5476a63f43298619381e8f7f609448f3bda32b26d7c9042b57a427a6dedd1c7fdca1a01ccbe8771b4311ed534079b676c64eec7 + graceful-fs: ^4.2.4 + retry: ^0.12.0 + signal-exit: ^3.0.2 + checksum: 00078ee6a61c216a56a6140c7d2a98c6c733b3678503002dc073ab8beca5d50ca271de4c85fca13b9b8ee2ff546c36674d1850509b84a04a5d0363bcb8638939 languageName: node linkType: hard -"property-expr@npm:^2.0.4": - version: 2.0.4 - resolution: "property-expr@npm:2.0.4" - checksum: 7ac142e189f0feef685f327f582efe13bfbc24a0b6e2328afdb38520bc140caa5f91dfa9529f2539b4468d85dc83a593e1ef0e0f7401b525368bb634b323bf54 +"properties-reader@npm:^2.3.0": + version: 2.3.0 + resolution: "properties-reader@npm:2.3.0" + dependencies: + mkdirp: ^1.0.4 + checksum: cbf59e862dc507f8ce1f8d7641ed9737119f16a1d4dad8e79f17b303aaca1c6af7d36ddfef0f649cab4d200ba4334ac159af0b238f6978a085f5b1b5126b6cc3 + languageName: node + linkType: hard + +"property-expr@npm:^2.0.5": + version: 2.0.6 + resolution: "property-expr@npm:2.0.6" + checksum: 89977f4bb230736c1876f460dd7ca9328034502fd92e738deb40516d16564b850c0bbc4e052c3df88b5b8cd58e51c93b46a94bea049a3f23f4a022c038864cab languageName: node linkType: hard @@ -42443,13 +42515,17 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.15.0": - version: 2.15.5 - resolution: "streamx@npm:2.15.5" +"streamx@npm:^2.13.0, streamx@npm:^2.15.0": + version: 2.16.1 + resolution: "streamx@npm:2.16.1" dependencies: + bare-events: ^2.2.0 fast-fifo: ^1.1.0 queue-tick: ^1.0.1 - checksum: 52e0ec94026d67c9e2e2e1090f05e5b138c2f2822462d9a8ef4a4805625a31d103e55ea5267fcd9bfe041374926424e42aec2dda28a85cb9de42c2a16d416d94 + dependenciesMeta: + bare-events: + optional: true + checksum: 6bbb4c38c0ab6ddbe0857d55e72f71288f308f2a9f4413b7b07391cdf9f94232ffc2bbe40a1212d2e09634ecdbd5052b444c73cc8d67ae1c97e2b7e553dad559 languageName: node linkType: hard @@ -43124,7 +43200,7 @@ __metadata: languageName: node linkType: hard -"tar-fs@npm:^2.0.0, tar-fs@npm:^2.1.1": +"tar-fs@npm:^2.0.0": version: 2.1.1 resolution: "tar-fs@npm:2.1.1" dependencies: @@ -43136,6 +43212,23 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:^3.0.4": + version: 3.0.5 + resolution: "tar-fs@npm:3.0.5" + dependencies: + bare-fs: ^2.1.1 + bare-path: ^2.1.0 + pump: ^3.0.0 + tar-stream: ^3.1.5 + dependenciesMeta: + bare-fs: + optional: true + bare-path: + optional: true + checksum: e31c7e3e525fec0afecdec1cac58071809e396187725f2eba442f08a4c5649c8cd6b7ce25982f9a91bb0f055628df47c08177dd2ea4f5dafd3c22f42f8da8f00 + languageName: node + linkType: hard + "tar-fs@npm:~2.0.1": version: 2.0.1 resolution: "tar-fs@npm:2.0.1" @@ -43161,14 +43254,14 @@ __metadata: languageName: node linkType: hard -"tar-stream@npm:^3.0.0": - version: 3.1.6 - resolution: "tar-stream@npm:3.1.6" +"tar-stream@npm:^3.0.0, tar-stream@npm:^3.1.5": + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" dependencies: b4a: ^1.6.4 fast-fifo: ^1.2.0 streamx: ^2.15.0 - checksum: f3627f918581976e954ff03cb8d370551053796b82564f8c7ca8fac84c48e4d042026d0854fc222171a34ff9c682b72fae91be9c9b0a112d4c54f9e4f443e9c5 + checksum: 6393a6c19082b17b8dcc8e7fd349352bb29b4b8bfe1075912b91b01743ba6bb4298f5ff0b499a3bbaf82121830e96a1a59d4f21a43c0df339e54b01789cb8cc6 languageName: node linkType: hard @@ -43318,23 +43411,26 @@ __metadata: languageName: node linkType: hard -"testcontainers@npm:^8.1.2": - version: 8.16.0 - resolution: "testcontainers@npm:8.16.0" +"testcontainers@npm:^10.0.0": + version: 10.7.1 + resolution: "testcontainers@npm:10.7.1" dependencies: "@balena/dockerignore": ^1.0.2 - "@types/archiver": ^5.3.1 - "@types/dockerode": ^3.3.8 - archiver: ^5.3.1 + "@types/dockerode": ^3.3.21 + archiver: ^5.3.2 + async-lock: ^1.4.0 byline: ^5.0.0 debug: ^4.3.4 - docker-compose: ^0.23.17 - dockerode: ^3.3.1 + docker-compose: ^0.24.2 + dockerode: ^3.3.5 get-port: ^5.1.1 - properties-reader: ^2.2.0 + node-fetch: ^2.7.0 + proper-lockfile: ^4.1.2 + properties-reader: ^2.3.0 ssh-remote-port-forward: ^1.0.4 - tar-fs: ^2.1.1 - checksum: 2fb8250591691a4bd86640b53e13236ad507ba9e03ac3043683de5e9dd632bc29d52827c22ccfe2b0d28dec6896cbaa56dcb153ce65f7f74212ddefc204e8d6a + tar-fs: ^3.0.4 + tmp: ^0.2.1 + checksum: 3ecb439914fab1147943d7d97e4021309fa69f3fcdbc153f4cf3fcf1feb03415da402fb914811189a3606409d58de1174910e144fb4c6c7e3510cc2cb59911b2 languageName: node linkType: hard @@ -43447,6 +43543,13 @@ __metadata: languageName: node linkType: hard +"tiny-case@npm:^1.0.3": + version: 1.0.3 + resolution: "tiny-case@npm:1.0.3" + checksum: 3f7a30c39d5b0e1bc097b0b271bec14eb5b836093db034f35a0de26c14422380b50dc12bfd37498cf35b192f5df06f28a710712c87ead68872a9e37ad6f6049d + languageName: node + linkType: hard + "tiny-emitter@npm:^2.0.0": version: 2.1.0 resolution: "tiny-emitter@npm:2.1.0" @@ -43477,6 +43580,15 @@ __metadata: languageName: node linkType: hard +"tmp@npm:^0.2.1": + version: 0.2.1 + resolution: "tmp@npm:0.2.1" + dependencies: + rimraf: ^3.0.0 + checksum: 8b1214654182575124498c87ca986ac53dc76ff36e8f0e0b67139a8d221eaecfdec108c0e6ec54d76f49f1f72ab9325500b246f562b926f85bcdfca8bf35df9e + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -44833,7 +44945,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:8.3.2, uuid@npm:^8.0.0, uuid@npm:^8.2.0, uuid@npm:^8.3.0, uuid@npm:^8.3.2": +"uuid@npm:8.3.2, uuid@npm:^8.0.0, uuid@npm:^8.3.0, uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" bin: @@ -46090,7 +46202,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:2.3.4, yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3": +"yaml@npm:2.3.4": version: 2.3.4 resolution: "yaml@npm:2.3.4" checksum: e6d1dae1c6383bcc8ba11796eef3b8c02d5082911c6723efeeb5ba50fc8e881df18d645e64de68e421b577296000bea9c75d6d9097c2f6699da3ae0406c030d8 @@ -46104,6 +46216,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3": + version: 2.4.0 + resolution: "yaml@npm:2.4.0" + bin: + yaml: bin.mjs + checksum: 3c25ebae34ee702af772ebbd1855a980b1487cd21d6220d952592edb4f7d89322aafd14753d99924ba7076eb4c5b3d809c64bb532402b01af280f7af674277f1 + languageName: node + linkType: hard + "yargs-parser@npm:^18.1.2, yargs-parser@npm:^18.1.3": version: 18.1.3 resolution: "yargs-parser@npm:18.1.3" @@ -46177,13 +46298,13 @@ __metadata: languageName: node linkType: hard -"yauzl@npm:^2.10.0": - version: 2.10.0 - resolution: "yauzl@npm:2.10.0" +"yauzl@npm:^3.0.0": + version: 3.1.0 + resolution: "yauzl@npm:3.1.0" dependencies: buffer-crc32: ~0.2.3 - fd-slicer: ~1.1.0 - checksum: 7f21fe0bbad6e2cb130044a5d1d0d5a0e5bf3d8d4f8c4e6ee12163ce798fee3de7388d22a7a0907f563ac5f9d40f8699a223d3d5c1718da90b0156da6904022b + pend: ~1.2.0 + checksum: 0464b49b0c10f0ab19136c917358b025c7f86bab0699a1f8afd681e09f3782688d3106b4e7eadfcc81e54779cca5c6de0843fabf5ceb07e1638ec2fd8371d0b7 languageName: node linkType: hard @@ -46272,18 +46393,15 @@ __metadata: languageName: node linkType: hard -"yup@npm:^0.32.9": - version: 0.32.11 - resolution: "yup@npm:0.32.11" +"yup@npm:^1.0.0": + version: 1.3.3 + resolution: "yup@npm:1.3.3" dependencies: - "@babel/runtime": ^7.15.4 - "@types/lodash": ^4.14.175 - lodash: ^4.17.21 - lodash-es: ^4.17.21 - nanoclone: ^0.2.1 - property-expr: ^2.0.4 + property-expr: ^2.0.5 + tiny-case: ^1.0.3 toposort: ^2.0.2 - checksum: 43a16786b47cc910fed4891cebdd89df6d6e31702e9462e8f969c73eac88551ce750732608012201ea6b93802c8847cb0aa27b5d57370640f4ecf30f9f97d4b0 + type-fest: ^2.19.0 + checksum: 7b9e19fedc85deb8cffd6b24617f79542c904f11a9c4ca8bb72fb003e61bd6c5278cc799fa9421c6a1a96ff04e7117bf0c000c9f10b543d6cd32a1e35e8f5f65 languageName: node linkType: hard