diff --git a/.changeset/calm-cups-smile.md b/.changeset/calm-cups-smile.md new file mode 100644 index 0000000000..68fcfb2d1a --- /dev/null +++ b/.changeset/calm-cups-smile.md @@ -0,0 +1,60 @@ +--- +'@backstage/plugin-api-docs-module-protoc-gen-doc': patch +'@backstage/plugin-catalog-unprocessed-entities': patch +'@backstage/plugin-github-pull-requests-board': patch +'@backstage/integration-react': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-microsoft-calendar': patch +'@backstage/plugin-newrelic-dashboard': patch +'@backstage/plugin-entity-validation': patch +'@backstage/plugin-apollo-explorer': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-graphql-voyager': patch +'@backstage/plugin-apache-airflow': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-octopus-deploy': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-github-issues': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-code-climate': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-azure-sites': patch +'@backstage/plugin-firehydrant': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-home-react': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-dynatrace': patch +'@backstage/plugin-gcalendar': patch +'@backstage/plugin-org-react': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-xcmetrics': patch +'@backstage/plugin-airbrake': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-linguist': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-opencost': patch +'@backstage/plugin-periskop': patch +'@backstage/plugin-playlist': patch +'@backstage/plugin-puppetdb': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bazaar': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-nomad': patch +'@backstage/plugin-vault': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-org': patch +--- + +Moved `@types/react` to be a regular dependency diff --git a/.changeset/catalog-everybody-to-the-limit.md b/.changeset/catalog-everybody-to-the-limit.md new file mode 100644 index 0000000000..4c0177820e --- /dev/null +++ b/.changeset/catalog-everybody-to-the-limit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fix to the `limit` parameter on entity queries. diff --git a/.changeset/chatty-goats-mate.md b/.changeset/chatty-goats-mate.md new file mode 100644 index 0000000000..2d4911c603 --- /dev/null +++ b/.changeset/chatty-goats-mate.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-common': patch +--- + +Add AWS Annotations to Kubernetes Cluster Resource diff --git a/.changeset/chilly-parrots-yell.md b/.changeset/chilly-parrots-yell.md new file mode 100644 index 0000000000..fe4c760923 --- /dev/null +++ b/.changeset/chilly-parrots-yell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Added title to props for `HasCards`. diff --git a/.changeset/create-app-1692704146.md b/.changeset/create-app-1692704146.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1692704146.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/curvy-roses-march.md b/.changeset/curvy-roses-march.md new file mode 100644 index 0000000000..cd38808165 --- /dev/null +++ b/.changeset/curvy-roses-march.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Migrated the GitLab auth provider to be implemented using the new `@backstage/plugin-auth-backend-module-gitlab-provider` module. diff --git a/.changeset/cyan-roses-relate.md b/.changeset/cyan-roses-relate.md new file mode 100644 index 0000000000..e6dbeb6ed9 --- /dev/null +++ b/.changeset/cyan-roses-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added the ability to create a plain backend module with the `new` command. diff --git a/.changeset/dirty-carrots-notice.md b/.changeset/dirty-carrots-notice.md new file mode 100644 index 0000000000..ccc31c25ca --- /dev/null +++ b/.changeset/dirty-carrots-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added `authPlugin` export for the new backend system. The plugin does not include any built-in auth providers, they must instead be added by installing additional modules, for example `authModuleGoogleProvider` from `@backstage/plugin-auth-backend-module-google-provider`. diff --git a/.changeset/dirty-years-travel.md b/.changeset/dirty-years-travel.md new file mode 100644 index 0000000000..e6e5b8a608 --- /dev/null +++ b/.changeset/dirty-years-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Added support for LCOV coverage reports diff --git a/.changeset/dry-wasps-occur.md b/.changeset/dry-wasps-occur.md new file mode 100644 index 0000000000..0c7d231708 --- /dev/null +++ b/.changeset/dry-wasps-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-gcp-iap-provider': minor +--- + +New module for `@backstage/plugin-auth-backend` that adds a GCP IAP auth provider. diff --git a/.changeset/dull-tables-joke.md b/.changeset/dull-tables-joke.md new file mode 100644 index 0000000000..e987377023 --- /dev/null +++ b/.changeset/dull-tables-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fixed an issue where `EntitySwitch` was preventing the display of entity errors. diff --git a/.changeset/eight-radios-listen.md b/.changeset/eight-radios-listen.md new file mode 100644 index 0000000000..0421a561c0 --- /dev/null +++ b/.changeset/eight-radios-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fixed bug in styles that caused next and previous links in footer to overlap page content. diff --git a/.changeset/eighty-fans-provide.md b/.changeset/eighty-fans-provide.md new file mode 100644 index 0000000000..b98bc39924 --- /dev/null +++ b/.changeset/eighty-fans-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fixed an issue causing `StructuredMetadataTable` to crash in case metadata contained `null` values. diff --git a/.changeset/empty-rings-type.md b/.changeset/empty-rings-type.md new file mode 100644 index 0000000000..2aa41054f9 --- /dev/null +++ b/.changeset/empty-rings-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add examples for `publish:github` and `publish:gitlab` scaffolder actions. diff --git a/.changeset/fair-paws-flash.md b/.changeset/fair-paws-flash.md new file mode 100644 index 0000000000..d9a7b1827e --- /dev/null +++ b/.changeset/fair-paws-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': patch +--- + +Improve consistency of results from the `AzureDevOpsEntityProvider`. diff --git a/.changeset/few-nails-smile.md b/.changeset/few-nails-smile.md new file mode 100644 index 0000000000..2fad9a0b40 --- /dev/null +++ b/.changeset/few-nails-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-entity-feedback': patch +--- + +Improve README to note that Backstage identity is required to be configured diff --git a/.changeset/fresh-candles-warn.md b/.changeset/fresh-candles-warn.md new file mode 100644 index 0000000000..49ca2e5402 --- /dev/null +++ b/.changeset/fresh-candles-warn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-node': patch +--- + +The `BackstageIdentityResponse` interface now has an optional `expiresInSeconds` field that can be used to signal session expiration. The `prepareBackstageIdentityResponse` utility will now also read the expiration from the provided token, and include it in the response. diff --git a/.changeset/great-chairs-swim.md b/.changeset/great-chairs-swim.md new file mode 100644 index 0000000000..e6b8ab0882 --- /dev/null +++ b/.changeset/great-chairs-swim.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. diff --git a/.changeset/grumpy-papayas-tie.md b/.changeset/grumpy-papayas-tie.md new file mode 100644 index 0000000000..48580673d8 --- /dev/null +++ b/.changeset/grumpy-papayas-tie.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Additional fix for Gitiles auth links diff --git a/.changeset/healthy-tools-count.md b/.changeset/healthy-tools-count.md new file mode 100644 index 0000000000..5d3f04159f --- /dev/null +++ b/.changeset/healthy-tools-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Deprecated several exports that are now available from `@backstage/plugin-auth-node` instead. diff --git a/.changeset/hungry-dots-design.md b/.changeset/hungry-dots-design.md new file mode 100644 index 0000000000..0dd8d415e9 --- /dev/null +++ b/.changeset/hungry-dots-design.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/backend-common': patch +--- + +Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. diff --git a/.changeset/late-cows-turn.md b/.changeset/late-cows-turn.md new file mode 100644 index 0000000000..7fa775ce22 --- /dev/null +++ b/.changeset/late-cows-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed a link to the frontend Backstage plugin that had pointed to itself. diff --git a/.changeset/lazy-rice-rule.md b/.changeset/lazy-rice-rule.md new file mode 100644 index 0000000000..1c3a3c3250 --- /dev/null +++ b/.changeset/lazy-rice-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixed the plugin and module ID of the alpha `catalogModuleTemplateKind` export. diff --git a/.changeset/lovely-years-sniff.md b/.changeset/lovely-years-sniff.md new file mode 100644 index 0000000000..09b86f7112 --- /dev/null +++ b/.changeset/lovely-years-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. diff --git a/.changeset/lucky-chicken-lie.md b/.changeset/lucky-chicken-lie.md new file mode 100644 index 0000000000..6edb3a36f6 --- /dev/null +++ b/.changeset/lucky-chicken-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-gitlab-provider': minor +--- + +New module for `@backstage/plugin-auth-backend` that adds a GitLab auth provider. diff --git a/.changeset/mean-carpets-provide.md b/.changeset/mean-carpets-provide.md new file mode 100644 index 0000000000..b5e9f551d4 --- /dev/null +++ b/.changeset/mean-carpets-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': patch +--- + +Remove duplications from Azure search before committing the new locations to the catalog. diff --git a/.changeset/mighty-crabs-cheat.md b/.changeset/mighty-crabs-cheat.md new file mode 100644 index 0000000000..ef2abbada4 --- /dev/null +++ b/.changeset/mighty-crabs-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added the ability to disable the built-in auth providers by passing `disableDefaultProviderFactories` to `createRouter`. diff --git a/.changeset/nasty-forks-beg.md b/.changeset/nasty-forks-beg.md new file mode 100644 index 0000000000..0662431453 --- /dev/null +++ b/.changeset/nasty-forks-beg.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-search-react': minor +'@backstage/plugin-search': minor +--- + +The SearchPage component can now be configured via app-config.yaml with default query parameters to define how it behaves when it is first loaded or reset. Check out the following example: + +```yaml +search: + query: + pageLimit: 50 +``` + +Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. diff --git a/.changeset/new-sheep-cover.md b/.changeset/new-sheep-cover.md new file mode 100644 index 0000000000..390de1a9b3 --- /dev/null +++ b/.changeset/new-sheep-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-linguist-backend': patch +--- + +When creating the router using `createRouterFromConfig` or using the plugin for the new backend system the `linguist.useSourceLocation` configuration is now optional. diff --git a/.changeset/nice-forks-remain.md b/.changeset/nice-forks-remain.md new file mode 100644 index 0000000000..6d833ff283 --- /dev/null +++ b/.changeset/nice-forks-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage': patch +--- + +Use fetchApi to ensure authorization is used when fetching code-coverage data diff --git a/.changeset/ninety-news-visit.md b/.changeset/ninety-news-visit.md new file mode 100644 index 0000000000..1b990f9ac7 --- /dev/null +++ b/.changeset/ninety-news-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +The algorithm used when generating Backstage tokens can be configured via `auth.identityTokenAlgorithm`. diff --git a/.changeset/old-geese-drive.md b/.changeset/old-geese-drive.md new file mode 100644 index 0000000000..ddaa7ad30a --- /dev/null +++ b/.changeset/old-geese-drive.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Extended `mockService` to also include mocked variants, for example `mockServices.lifecycle.mock()`. The returned mocked implementation will have a `factory` property which is a service factory for itself. You can also pass a partial implementation of the service to the mock function to use a mock implementation of specific methods. diff --git a/.changeset/orange-houses-fly.md b/.changeset/orange-houses-fly.md new file mode 100644 index 0000000000..546484304c --- /dev/null +++ b/.changeset/orange-houses-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Gitiles: Fixed auth prefix issue diff --git a/.changeset/pink-cups-rescue.md b/.changeset/pink-cups-rescue.md new file mode 100644 index 0000000000..abb69e88b7 --- /dev/null +++ b/.changeset/pink-cups-rescue.md @@ -0,0 +1,28 @@ +--- +'@backstage/plugin-auth-node': minor +--- + +Introduced a new system for building auth providers for `@backstage/plugin-auth-backend`, which both increases the amount of code re-use across providers, and also works better with the new backend system. + +Many existing types have been moved from `@backstage/plugin-auth-backend` in order to avoid a direct dependency on the plugin from modules. + +Auth provider integrations are now primarily implemented through a pattern of creating "authenticators", which are in turn specific to each kind of integrations. Initially there are two types: `createOAuthAuthenticator` and `createProxyAuthenticator`. These come paired with functions that let you create the corresponding route handlers, `createOAuthRouteHandlers` and `createProxyAuthRouteHandlers`, as well as provider factories, `createOAuthProviderFactory` and `createProxyAuthProviderFactory`. This new authenticator pattern allows the sign-in logic to be separated from the auth integration logic, allowing it to be completely re-used across all providers of the same kind. + +The new provider factories also implement a new declarative way to configure sign-in resolvers, rather than configuration through code. Sign-in resolvers can now be configured through the `resolvers` configuration key, where the first resolver that provides an identity will be used, for example: + +```yaml +auth: + providers: + google: + development: + clientId: ... + clientSecret: ... + signIn: + resolvers: + - resolver: emailMatchingUserEntityAnnotation + - resolver: emailLocalPartMatchingUserEntityName +``` + +These configurable resolvers are created with a new `createSignInResolverFactory` function, which creates a sign-in resolver factory, optionally with an options schema that will be used both when configuring the sign-in resolver through configuration and code. + +The internal helpers from `@backstage/plugin-auth-backend` that were used to implement auth providers using passport strategies have now also been made available as public API, through `PassportHelpers` and `PassportOAuthAuthenticatorHelper`. diff --git a/.changeset/poor-years-appear.md b/.changeset/poor-years-appear.md new file mode 100644 index 0000000000..d5b53a7051 --- /dev/null +++ b/.changeset/poor-years-appear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-entity-feedback-backend': patch +--- + +Improve backend logging if method calls fail diff --git a/.changeset/popular-readers-allow.md b/.changeset/popular-readers-allow.md new file mode 100644 index 0000000000..b89e0c264e --- /dev/null +++ b/.changeset/popular-readers-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Added the optional `expiresAt` field that may now be part of a `BackstageIdentityResponse`. diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..5784d5be24 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,286 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "example-app": "0.2.86", + "@backstage/app-defaults": "1.4.2", + "example-backend": "0.2.86", + "@backstage/backend-app-api": "0.5.0", + "@backstage/backend-common": "0.19.2", + "@backstage/backend-defaults": "0.2.0", + "@backstage/backend-dev-utils": "0.1.1", + "example-backend-next": "0.0.14", + "@backstage/backend-openapi-utils": "0.0.3", + "@backstage/backend-plugin-api": "0.6.0", + "@backstage/backend-tasks": "0.5.5", + "@backstage/backend-test-utils": "0.2.0", + "@backstage/catalog-client": "1.4.3", + "@backstage/catalog-model": "1.4.1", + "@backstage/cli": "0.22.10", + "@backstage/cli-common": "0.1.12", + "@backstage/cli-node": "0.1.3", + "@backstage/codemods": "0.1.45", + "@backstage/config": "1.0.8", + "@backstage/config-loader": "1.4.0", + "@backstage/core-app-api": "1.9.1", + "@backstage/core-components": "0.13.4", + "@backstage/core-plugin-api": "1.5.3", + "@backstage/create-app": "0.5.4", + "@backstage/dev-utils": "1.0.18", + "e2e-test": "0.2.6", + "@backstage/errors": "1.2.1", + "@backstage/eslint-plugin": "0.1.3", + "@backstage/integration": "1.6.0", + "@backstage/integration-aws-node": "0.1.5", + "@backstage/integration-react": "1.1.16", + "@backstage/release-manifests": "0.0.9", + "@backstage/repo-tools": "0.3.3", + "@techdocs/cli": "1.4.5", + "techdocs-cli-embedded-app": "0.2.85", + "@backstage/test-utils": "1.4.2", + "@backstage/theme": "0.4.1", + "@backstage/types": "1.1.0", + "@backstage/version-bridge": "1.0.4", + "@backstage/plugin-adr": "0.6.4", + "@backstage/plugin-adr-backend": "0.3.6", + "@backstage/plugin-adr-common": "0.2.12", + "@backstage/plugin-airbrake": "0.3.21", + "@backstage/plugin-airbrake-backend": "0.2.21", + "@backstage/plugin-allure": "0.1.37", + "@backstage/plugin-analytics-module-ga": "0.1.32", + "@backstage/plugin-analytics-module-ga4": "0.1.3", + "@backstage/plugin-analytics-module-newrelic-browser": "0.0.1", + "@backstage/plugin-apache-airflow": "0.2.14", + "@backstage/plugin-api-docs": "0.9.7", + "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.2", + "@backstage/plugin-apollo-explorer": "0.1.14", + "@backstage/plugin-app-backend": "0.3.48", + "@backstage/plugin-app-node": "0.1.0", + "@backstage/plugin-auth-backend": "0.18.6", + "@backstage/plugin-auth-node": "0.2.17", + "@backstage/plugin-azure-devops": "0.3.3", + "@backstage/plugin-azure-devops-backend": "0.3.27", + "@backstage/plugin-azure-devops-common": "0.3.0", + "@backstage/plugin-azure-sites": "0.1.10", + "@backstage/plugin-azure-sites-backend": "0.1.10", + "@backstage/plugin-azure-sites-common": "0.1.0", + "@backstage/plugin-badges": "0.2.45", + "@backstage/plugin-badges-backend": "0.2.3", + "@backstage/plugin-bazaar": "0.2.12", + "@backstage/plugin-bazaar-backend": "0.2.11", + "@backstage/plugin-bitbucket-cloud-common": "0.2.9", + "@backstage/plugin-bitrise": "0.1.48", + "@backstage/plugin-catalog": "1.12.1", + "@backstage/plugin-catalog-backend": "1.12.0", + "@backstage/plugin-catalog-backend-module-aws": "0.2.3", + "@backstage/plugin-catalog-backend-module-azure": "0.1.19", + "@backstage/plugin-catalog-backend-module-bitbucket": "0.2.15", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.15", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.13", + "@backstage/plugin-catalog-backend-module-gcp": "0.1.0", + "@backstage/plugin-catalog-backend-module-gerrit": "0.1.16", + "@backstage/plugin-catalog-backend-module-github": "0.3.3", + "@backstage/plugin-catalog-backend-module-gitlab": "0.2.4", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.4.1", + "@backstage/plugin-catalog-backend-module-ldap": "0.5.15", + "@backstage/plugin-catalog-backend-module-msgraph": "0.5.7", + "@backstage/plugin-catalog-backend-module-openapi": "0.1.14", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.1.5", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.2.0", + "@backstage/plugin-catalog-common": "1.0.15", + "@internal/plugin-catalog-customized": "0.0.13", + "@backstage/plugin-catalog-graph": "0.2.33", + "@backstage/plugin-catalog-graphql": "0.3.22", + "@backstage/plugin-catalog-import": "0.9.11", + "@backstage/plugin-catalog-node": "1.4.1", + "@backstage/plugin-catalog-react": "1.8.1", + "@backstage/plugin-catalog-unprocessed-entities": "0.1.2", + "@backstage/plugin-cicd-statistics": "0.1.23", + "@backstage/plugin-cicd-statistics-module-gitlab": "0.1.17", + "@backstage/plugin-circleci": "0.3.21", + "@backstage/plugin-cloudbuild": "0.3.21", + "@backstage/plugin-code-climate": "0.1.21", + "@backstage/plugin-code-coverage": "0.2.14", + "@backstage/plugin-code-coverage-backend": "0.2.14", + "@backstage/plugin-codescene": "0.1.16", + "@backstage/plugin-config-schema": "0.1.44", + "@backstage/plugin-cost-insights": "0.12.10", + "@backstage/plugin-cost-insights-common": "0.1.1", + "@backstage/plugin-devtools": "0.1.3", + "@backstage/plugin-devtools-backend": "0.1.3", + "@backstage/plugin-devtools-common": "0.1.3", + "@backstage/plugin-dynatrace": "7.0.1", + "@backstage/plugin-entity-feedback": "0.2.4", + "@backstage/plugin-entity-feedback-backend": "0.1.6", + "@backstage/plugin-entity-feedback-common": "0.1.2", + "@backstage/plugin-entity-validation": "0.1.6", + "@backstage/plugin-events-backend": "0.2.9", + "@backstage/plugin-events-backend-module-aws-sqs": "0.2.3", + "@backstage/plugin-events-backend-module-azure": "0.1.10", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.1.10", + "@backstage/plugin-events-backend-module-gerrit": "0.1.10", + "@backstage/plugin-events-backend-module-github": "0.1.10", + "@backstage/plugin-events-backend-module-gitlab": "0.1.10", + "@backstage/plugin-events-backend-test-utils": "0.1.10", + "@backstage/plugin-events-node": "0.2.9", + "@internal/plugin-todo-list": "1.0.16", + "@internal/plugin-todo-list-backend": "1.0.16", + "@internal/plugin-todo-list-common": "1.0.12", + "@backstage/plugin-explore": "0.4.7", + "@backstage/plugin-explore-backend": "0.0.10", + "@backstage/plugin-explore-common": "0.0.1", + "@backstage/plugin-explore-react": "0.0.30", + "@backstage/plugin-firehydrant": "0.2.5", + "@backstage/plugin-fossa": "0.2.53", + "@backstage/plugin-gcalendar": "0.3.17", + "@backstage/plugin-gcp-projects": "0.3.40", + "@backstage/plugin-git-release-manager": "0.3.34", + "@backstage/plugin-github-actions": "0.6.2", + "@backstage/plugin-github-deployments": "0.1.52", + "@backstage/plugin-github-issues": "0.2.10", + "@backstage/plugin-github-pull-requests-board": "0.1.15", + "@backstage/plugin-gitops-profiles": "0.3.39", + "@backstage/plugin-gocd": "0.1.27", + "@backstage/plugin-graphiql": "0.2.53", + "@backstage/plugin-graphql-backend": "0.1.38", + "@backstage/plugin-graphql-voyager": "0.1.6", + "@backstage/plugin-home": "0.5.5", + "@backstage/plugin-home-react": "0.1.2", + "@backstage/plugin-ilert": "0.2.10", + "@backstage/plugin-jenkins": "0.8.3", + "@backstage/plugin-jenkins-backend": "0.2.3", + "@backstage/plugin-jenkins-common": "0.1.18", + "@backstage/plugin-kafka": "0.3.21", + "@backstage/plugin-kafka-backend": "0.2.41", + "@backstage/plugin-kubernetes": "0.10.0", + "@backstage/plugin-kubernetes-backend": "0.11.3", + "@backstage/plugin-kubernetes-common": "0.6.5", + "@backstage/plugin-lighthouse": "0.4.6", + "@backstage/plugin-lighthouse-backend": "0.2.4", + "@backstage/plugin-lighthouse-common": "0.1.2", + "@backstage/plugin-linguist": "0.1.6", + "@backstage/plugin-linguist-backend": "0.4.0", + "@backstage/plugin-linguist-common": "0.1.1", + "@backstage/plugin-microsoft-calendar": "0.1.6", + "@backstage/plugin-newrelic": "0.3.39", + "@backstage/plugin-newrelic-dashboard": "0.2.14", + "@backstage/plugin-nomad": "0.1.2", + "@backstage/plugin-nomad-backend": "0.1.2", + "@backstage/plugin-octopus-deploy": "0.2.3", + "@backstage/plugin-org": "0.6.11", + "@backstage/plugin-org-react": "0.1.10", + "@backstage/plugin-pagerduty": "0.6.2", + "@backstage/plugin-periskop": "0.1.19", + "@backstage/plugin-periskop-backend": "0.1.19", + "@backstage/plugin-permission-backend": "0.5.23", + "@backstage/plugin-permission-common": "0.7.7", + "@backstage/plugin-permission-node": "0.7.11", + "@backstage/plugin-permission-react": "0.4.14", + "@backstage/plugin-playlist": "0.1.13", + "@backstage/plugin-playlist-backend": "0.3.4", + "@backstage/plugin-playlist-common": "0.1.9", + "@backstage/plugin-proxy-backend": "0.3.0", + "@backstage/plugin-puppetdb": "0.1.4", + "@backstage/plugin-rollbar": "0.4.21", + "@backstage/plugin-rollbar-backend": "0.1.45", + "@backstage/plugin-scaffolder": "1.14.2", + "@backstage/plugin-scaffolder-backend": "1.16.0", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.2.1", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.24", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.2.3", + "@backstage/plugin-scaffolder-backend-module-rails": "0.4.17", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.8", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.21", + "@backstage/plugin-scaffolder-common": "1.4.0", + "@backstage/plugin-scaffolder-node": "0.2.0", + "@backstage/plugin-scaffolder-react": "1.5.2", + "@backstage/plugin-search": "1.3.4", + "@backstage/plugin-search-backend": "1.4.0", + "@backstage/plugin-search-backend-module-catalog": "0.1.4", + "@backstage/plugin-search-backend-module-elasticsearch": "1.3.3", + "@backstage/plugin-search-backend-module-explore": "0.1.4", + "@backstage/plugin-search-backend-module-pg": "0.5.9", + "@backstage/plugin-search-backend-module-techdocs": "0.1.4", + "@backstage/plugin-search-backend-node": "1.2.4", + "@backstage/plugin-search-common": "1.2.5", + "@backstage/plugin-search-react": "1.6.4", + "@backstage/plugin-sentry": "0.5.6", + "@backstage/plugin-shortcuts": "0.3.13", + "@backstage/plugin-sonarqube": "0.7.2", + "@backstage/plugin-sonarqube-backend": "0.2.2", + "@backstage/plugin-sonarqube-react": "0.1.7", + "@backstage/plugin-splunk-on-call": "0.4.10", + "@backstage/plugin-stack-overflow": "0.1.19", + "@backstage/plugin-stack-overflow-backend": "0.2.4", + "@backstage/plugin-stackstorm": "0.1.5", + "@backstage/plugin-tech-insights": "0.3.13", + "@backstage/plugin-tech-insights-backend": "0.5.14", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.32", + "@backstage/plugin-tech-insights-common": "0.2.11", + "@backstage/plugin-tech-insights-node": "0.4.6", + "@backstage/plugin-tech-radar": "0.6.7", + "@backstage/plugin-techdocs": "1.6.6", + "@backstage/plugin-techdocs-addons-test-utils": "1.0.17", + "@backstage/plugin-techdocs-backend": "1.6.5", + "@backstage/plugin-techdocs-module-addons-contrib": "1.0.16", + "@backstage/plugin-techdocs-node": "1.7.4", + "@backstage/plugin-techdocs-react": "1.1.9", + "@backstage/plugin-todo": "0.2.23", + "@backstage/plugin-todo-backend": "0.2.0", + "@backstage/plugin-user-settings": "0.7.6", + "@backstage/plugin-user-settings-backend": "0.1.12", + "@backstage/plugin-vault": "0.1.15", + "@backstage/plugin-vault-backend": "0.3.4", + "@backstage/plugin-xcmetrics": "0.2.41", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.0.0", + "@backstage/plugin-auth-backend-module-github-provider": "0.0.0", + "@backstage/plugin-auth-backend-module-google-provider": "0.0.0", + "@backstage/plugin-opencost": "0.1.0" + }, + "changesets": [ + "calm-cups-smile", + "catalog-everybody-to-the-limit", + "chilly-parrots-yell", + "create-app-1692704146", + "cyan-roses-relate", + "dirty-carrots-notice", + "dirty-years-travel", + "dry-wasps-occur", + "dull-tables-joke", + "eight-radios-listen", + "empty-rings-type", + "fair-paws-flash", + "few-nails-smile", + "fresh-candles-warn", + "great-chairs-swim", + "healthy-tools-count", + "hungry-dots-design", + "late-cows-turn", + "lazy-rice-rule", + "lovely-years-sniff", + "mean-carpets-provide", + "mighty-crabs-cheat", + "new-sheep-cover", + "nice-forks-remain", + "ninety-news-visit", + "old-geese-drive", + "orange-houses-fly", + "pink-cups-rescue", + "poor-years-appear", + "popular-readers-allow", + "renovate-76185f9", + "repo-p-lisa", + "repo-tools-boo-descriptions", + "repo-tools-ci-toldya-so", + "six-wolves-admire", + "slow-ducks-hunt", + "slow-queens-visit", + "smart-islands-jog", + "strange-frogs-count", + "sweet-hairs-complain", + "sweet-pens-hunt", + "warm-llamas-bake", + "warm-panthers-lick", + "yellow-trains-protect" + ] +} diff --git a/.changeset/renovate-76185f9.md b/.changeset/renovate-76185f9.md new file mode 100644 index 0000000000..5e23f3cfda --- /dev/null +++ b/.changeset/renovate-76185f9.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Updated dependency `@types/pluralize` to `^0.0.30`. diff --git a/.changeset/repo-p-lisa.md b/.changeset/repo-p-lisa.md new file mode 100644 index 0000000000..d6a0fa56f4 --- /dev/null +++ b/.changeset/repo-p-lisa.md @@ -0,0 +1,7 @@ +--- +'@backstage/repo-tools': patch +--- + +The `generate-catalog-info` command now uses the first listed `CODEOWNER` as Component owner when initially +creating the `catalog-info.yaml` file. It continues to allow any one listed `CODEOWNER` when updating +entity metadata. diff --git a/.changeset/repo-tools-boo-descriptions.md b/.changeset/repo-tools-boo-descriptions.md new file mode 100644 index 0000000000..5973dd7127 --- /dev/null +++ b/.changeset/repo-tools-boo-descriptions.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Fixed a bug with the `generate-catalog-info` command that could cause the `--dry-run` flag to indicate changes to files when no changes would actually be made if the command were run without the flag. diff --git a/.changeset/repo-tools-ci-toldya-so.md b/.changeset/repo-tools-ci-toldya-so.md new file mode 100644 index 0000000000..20425fc631 --- /dev/null +++ b/.changeset/repo-tools-ci-toldya-so.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Added a `--ci` flag to the `generate-catalog-info` command. This flag behaves similarly to the same flag on `api-reports`: if `catalog-info.yaml` files would have been added or modified, then the process exits with status code `1`, and instructions are printed. diff --git a/.changeset/six-wolves-admire.md b/.changeset/six-wolves-admire.md new file mode 100644 index 0000000000..efc80826c2 --- /dev/null +++ b/.changeset/six-wolves-admire.md @@ -0,0 +1,23 @@ +--- +'@backstage/integration': minor +--- + +Added `AzureDevOpsCredentialsProvider` to support multiple Azure DevOps organizations and **deprecated** `AzureIntegrationConfig.credential` and `AzureIntegrationConfig.token` in favour of `AzureIntegrationConfig.credentials`. You can now use specific credentials for different Azure DevOps (Server) organizations by specifying the `organizations` field on a credential: + +```yaml +integrations: + azure: + - host: dev.azure.com + credentials: + - organizations: + - my-org + - my-other-org + clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} + - organizations: + - yet-another-org + personalAccessToken: ${PERSONAL_ACCESS_TOKEN} +``` + +See the [Azure integration documentation](https://backstage.io/docs/integrations/azure/locations) for more information. diff --git a/.changeset/slow-ducks-hunt.md b/.changeset/slow-ducks-hunt.md new file mode 100644 index 0000000000..0aac3056cf --- /dev/null +++ b/.changeset/slow-ducks-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added support for the `dev/index` entry point for backend plugins and modules. diff --git a/.changeset/slow-queens-visit.md b/.changeset/slow-queens-visit.md new file mode 100644 index 0000000000..2df948e477 --- /dev/null +++ b/.changeset/slow-queens-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-opencost': minor +--- + +New OpenCost plugin provides an port of the latest OpenCost UI to Backstage with updated dependencies. The plugin's README covers installation and configuration diff --git a/.changeset/smart-islands-jog.md b/.changeset/smart-islands-jog.md new file mode 100644 index 0000000000..50bc36191f --- /dev/null +++ b/.changeset/smart-islands-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fix OpenAPI schema for the facets endpoint diff --git a/.changeset/strange-frogs-count.md b/.changeset/strange-frogs-count.md new file mode 100644 index 0000000000..94fa6b9fb6 --- /dev/null +++ b/.changeset/strange-frogs-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add loading indicator to Table diff --git a/.changeset/sweet-hairs-complain.md b/.changeset/sweet-hairs-complain.md new file mode 100644 index 0000000000..56dede5617 --- /dev/null +++ b/.changeset/sweet-hairs-complain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-google-provider': minor +--- + +New module for `@backstage/plugin-auth-backend` that adds a Google auth provider. diff --git a/.changeset/sweet-pens-hunt.md b/.changeset/sweet-pens-hunt.md new file mode 100644 index 0000000000..03e79ecebb --- /dev/null +++ b/.changeset/sweet-pens-hunt.md @@ -0,0 +1,24 @@ +--- +'@backstage/config-loader': minor +--- + +Deep visibility now also applies to values that are not covered by the configuration schema. + +For example, given the following configuration schema: + +```ts +// plugins/a/config.schema.ts +export interface Config { + /** @deepVisibility frontend */ + a?: unknown; +} + +// plugins/a/config.schema.ts +export interface Config { + a?: { + b?: string; + }; +} +``` + +All values under `a` are now visible to the frontend, while previously only `a` and `a/b` would've been visible. diff --git a/.changeset/warm-llamas-bake.md b/.changeset/warm-llamas-bake.md new file mode 100644 index 0000000000..3da4a9b94d --- /dev/null +++ b/.changeset/warm-llamas-bake.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Do not unnecessarily notify subscribers when no-op updates to config happen diff --git a/.changeset/warm-panthers-lick.md b/.changeset/warm-panthers-lick.md new file mode 100644 index 0000000000..b06809fac1 --- /dev/null +++ b/.changeset/warm-panthers-lick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-github-provider': minor +--- + +New module for `@backstage/plugin-auth-backend` that adds a GitHub auth provider. diff --git a/.changeset/yellow-trains-protect.md b/.changeset/yellow-trains-protect.md new file mode 100644 index 0000000000..4b8c50ac3e --- /dev/null +++ b/.changeset/yellow-trains-protect.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-linguist-backend': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/backend-tasks': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Changes needed to support MySQL diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5c957807cf..f58291912d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,8 +4,10 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @backstage/maintainers -*/yarn.lock @backstage/maintainers @backstage-service +* @backstage/maintainers @backstage/reviewers +yarn.lock @backstage/maintainers @backstage/reviewers @backstage-service +*/yarn.lock @backstage/maintainers @backstage/reviewers @backstage-service +/.changeset/*.md /cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-maintainers /docs/assets/search @backstage/discoverability-maintainers /docs/features/search @backstage/discoverability-maintainers @@ -14,81 +16,67 @@ /packages/cli/src/commands/onboard @backstage/sharks /packages/techdocs-cli @backstage/techdocs-maintainers /packages/techdocs-cli-embedded-app @backstage/techdocs-maintainers -/plugins/adr @backstage/maintainers @kuangp -/plugins/adr-* @backstage/maintainers @kuangp -/plugins/allure @backstage/maintainers @deepak-bhardwaj-ps -/plugins/analytics-module-newrelic-browser @backstage/maintainers @jmezach -/plugins/apache-airflow @backstage/maintainers @cmpadden -/plugins/api-docs @backstage/maintainers @backstage/sda-se-reviewers -/plugins/azure-devops @backstage/maintainers @marleypowell @awanlin -/plugins/azure-devops-backend @backstage/maintainers @marleypowell @awanlin -/plugins/azure-devops-common @backstage/maintainers @marleypowell @awanlin -/plugins/bitbucket-cloud-common @backstage/maintainers @pjungermann -/plugins/bitrise @backstage/maintainers @backstage/sda-se-reviewers -/plugins/catalog @backstage/maintainers @backstage/catalog-maintainers -/plugins/catalog-* @backstage/maintainers @backstage/catalog-maintainers -/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/catalog-maintainers @pjungermann -/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/catalog-maintainers @pjungermann -/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/catalog-maintainers @pjungermann -/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/catalog-maintainers @tdabasinskas -/plugins/catalog-graph @backstage/maintainers @backstage/catalog-maintainers @backstage/sda-se-reviewers -/plugins/circleci @backstage/maintainers @adamdmharvey -/plugins/cloudbuild @backstage/maintainers @trivago/ebarrios -/plugins/code-coverage @backstage/maintainers @alde @nissayeva -/plugins/code-coverage-backend @backstage/maintainers @alde @nissayeva -/plugins/cost-insights @backstage/maintainers @backstage/silver-lining -/plugins/cost-insights-* @backstage/maintainers @backstage/silver-lining -/plugins/devtools @backstage/maintainers @awanlin -/plugins/devtools-backend @backstage/maintainers @awanlin -/plugins/devtools-common @backstage/maintainers @awanlin -/plugins/entity-feedback @backstage/maintainers @kuangp -/plugins/entity-feedback-* @backstage/maintainers @kuangp -/plugins/events-backend @backstage/maintainers @pjungermann -/plugins/events-backend-module-aws-sqs @backstage/maintainers @pjungermann -/plugins/events-backend-module-azure @backstage/maintainers @pjungermann -/plugins/events-backend-module-bitbucket-cloud @backstage/maintainers @pjungermann -/plugins/events-backend-module-gerrit @backstage/maintainers @pjungermann -/plugins/events-backend-module-github @backstage/maintainers @pjungermann -/plugins/events-backend-module-gitlab @backstage/maintainers @pjungermann -/plugins/events-backend-test-utils @backstage/maintainers @pjungermann -/plugins/events-node @backstage/maintainers @pjungermann -/plugins/explore @backstage/maintainers @backstage/sda-se-reviewers -/plugins/explore-react @backstage/maintainers @backstage/sda-se-reviewers -/plugins/fossa @backstage/maintainers @backstage/sda-se-reviewers -/plugins/gcalendar @backstage/maintainers @szubster @ptychu @kielosz @alexrybch -/plugins/git-release-manager @backstage/maintainers @erikengervall +/plugins/adr @backstage/maintainers @backstage/reviewers @kuangp +/plugins/adr-* @backstage/maintainers @backstage/reviewers @kuangp +/plugins/api-docs @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers +/plugins/azure-devops @backstage/maintainers @backstage/reviewers @awanlin +/plugins/azure-devops-backend @backstage/maintainers @backstage/reviewers @awanlin +/plugins/azure-devops-common @backstage/maintainers @backstage/reviewers @awanlin +/plugins/bitbucket-cloud-common @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/bitrise @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers +/plugins/catalog @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers +/plugins/catalog-* @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers +/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann +/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann +/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann +/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers +/plugins/catalog-graph @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @backstage/sda-se-reviewers +/plugins/circleci @backstage/maintainers @backstage/reviewers @adamdmharvey +/plugins/code-coverage @backstage/maintainers @backstage/reviewers @alde +/plugins/code-coverage-backend @backstage/maintainers @backstage/reviewers @alde +/plugins/cost-insights @backstage/maintainers @backstage/reviewers @backstage/silver-lining +/plugins/cost-insights-* @backstage/maintainers @backstage/reviewers @backstage/silver-lining +/plugins/devtools @backstage/maintainers @backstage/reviewers @awanlin +/plugins/devtools-backend @backstage/maintainers @backstage/reviewers @awanlin +/plugins/devtools-common @backstage/maintainers @backstage/reviewers @awanlin +/plugins/entity-feedback @backstage/maintainers @backstage/reviewers @kuangp +/plugins/entity-feedback-* @backstage/maintainers @backstage/reviewers @kuangp +/plugins/events-backend @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-aws-sqs @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-azure @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-bitbucket-cloud @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-gerrit @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-github @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-gitlab @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-test-utils @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-node @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/explore @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers +/plugins/explore-react @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers +/plugins/fossa @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers +/plugins/git-release-manager @backstage/maintainers @backstage/reviewers @erikengervall /plugins/home @backstage/discoverability-maintainers /plugins/home-* @backstage/discoverability-maintainers -/plugins/ilert @backstage/maintainers @yacut -/plugins/jenkins @backstage/maintainers @timja -/plugins/jenkins-backend @backstage/maintainers @timja -/plugins/kafka @backstage/maintainers @nirga @andrewthauer -/plugins/kafka-backend @backstage/maintainers @nirga @andrewthauer -/plugins/kubernetes @backstage/maintainers @backstage/kubernetes-maintainers -/plugins/kubernetes-* @backstage/maintainers @backstage/kubernetes-maintainers -/plugins/linguist @backstage/maintainers @awanlin -/plugins/linguist-backend @backstage/maintainers @awanlin -/plugins/linguist-common @backstage/maintainers @awanlin -/plugins/microsoft-calendar @backstage/maintainers @abhay-soni-developer @NishkarshRaj -/plugins/newrelic-dashboard @backstage/maintainers @mufaddal7 +/plugins/kafka @backstage/maintainers @backstage/reviewers @andrewthauer +/plugins/kafka-backend @backstage/maintainers @backstage/reviewers @andrewthauer +/plugins/kubernetes @backstage/maintainers @backstage/reviewers @backstage/kubernetes-maintainers +/plugins/kubernetes-* @backstage/maintainers @backstage/reviewers @backstage/kubernetes-maintainers +/plugins/linguist @backstage/maintainers @backstage/reviewers @awanlin +/plugins/linguist-backend @backstage/maintainers @backstage/reviewers @awanlin +/plugins/linguist-common @backstage/maintainers @backstage/reviewers @awanlin /plugins/permission-* @backstage/permission-maintainers -/plugins/playlist @backstage/maintainers @kuangp -/plugins/playlist-* @backstage/maintainers @kuangp -/plugins/puppetdb @backstage/maintainers @tdabasinskas -/plugins/rollbar @backstage/maintainers @andrewthauer -/plugins/rollbar-backend @backstage/maintainers @andrewthauer -/plugins/scaffolder-backend-module-rails @backstage/maintainers @angeliski -/plugins/scaffolder-backend-module-yeoman @backstage/maintainers @pawelmitka +/plugins/playlist @backstage/maintainers @backstage/reviewers @kuangp +/plugins/playlist-* @backstage/maintainers @backstage/reviewers @kuangp +/plugins/rollbar @backstage/maintainers @backstage/reviewers @andrewthauer +/plugins/rollbar-backend @backstage/maintainers @backstage/reviewers @andrewthauer /plugins/search @backstage/discoverability-maintainers /plugins/search-* @backstage/discoverability-maintainers -/plugins/sonarqube @backstage/maintainers @backstage/sda-se-reviewers +/plugins/sonarqube @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers /plugins/stack-overflow @backstage/discoverability-maintainers /plugins/stack-overflow-backend @backstage/discoverability-maintainers /plugins/techdocs @backstage/techdocs-maintainers /plugins/techdocs-* @backstage/techdocs-maintainers -/plugins/user-settings-backend @backstage/maintainers @backstage/sda-se-reviewers -/tech-insights-backend @backstage/maintainers @xantier @iain-b -/tech-insights-backend-module-jsonfc @backstage/maintainers @xantier @iain-b -/tech-insights-tech-insights-common @backstage/maintainers @xantier @iain-b -/tech-insights-tech-insights-node @backstage/maintainers @xantier @iain-b -yarn.lock @backstage/maintainers @backstage-service +/plugins/user-settings-backend @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers +/plugins/tech-insights-backend @backstage/maintainers @backstage/reviewers @xantier +/plugins/tech-insights-backend-module-jsonfc @backstage/maintainers @backstage/reviewers @xantier +/plugins/tech-insights-tech-insights-common @backstage/maintainers @backstage/reviewers @xantier +/plugins/tech-insights-tech-insights-node @backstage/maintainers @backstage/reviewers @xantier diff --git a/.github/uffizzi/uffizzi.production.app-config.yaml b/.github/uffizzi/uffizzi.production.app-config.yaml index 4f64e5ffcb..34f4c90f0b 100644 --- a/.github/uffizzi/uffizzi.production.app-config.yaml +++ b/.github/uffizzi/uffizzi.production.app-config.yaml @@ -280,3 +280,7 @@ gocd: permission: enabled: true + +search: + query: + pageLimit: 50 diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index 87dec287d7..8bfa90d726 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -30,7 +30,7 @@ jobs: --remote-repo-url=https://github.com/backstage/backstage env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - NODE_OPTIONS: --max-old-space-size=4096 + NODE_OPTIONS: --max-old-space-size=7168 # Above we run the `monitor` command, this runs the `test` command which is # the one that generates the SARIF report that we can upload to GitHub. @@ -45,7 +45,7 @@ jobs: --sarif-file-output=snyk.sarif env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - NODE_OPTIONS: --max-old-space-size=4096 + NODE_OPTIONS: --max-old-space-size=7168 - name: Upload Snyk report uses: github/codeql-action/upload-sarif@v2 with: diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index 22421925c7..6d662ea05f 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -27,3 +27,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} HUSKY: '0' + + - name: Discord notification + if: ${{ failure() }} + uses: Ilshidur/action-discord@0.3.2 + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + with: + args: 'Version Packages Sync Failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}' diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml index fc62d36563..a018537daf 100644 --- a/.github/workflows/uffizzi-build.yml +++ b/.github/workflows/uffizzi-build.yml @@ -4,6 +4,8 @@ on: types: [opened, synchronize, reopened, closed] paths-ignore: - 'microsite/**' + - 'storybook/**' + - 'contrib/**' - '*.md' jobs: diff --git a/OWNERS.md b/OWNERS.md index 66575e8e35..85f7fe545a 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -90,10 +90,15 @@ Team: @backstage/techdocs-maintainers Scope: The TechDocs plugin and related tooling -| Name | Organization | Team | GitHub | Discord | -| ------------------------ | ------------ | ------------ | ------------------------------------------------ | ------------------ | -| Morgan Bentell | Spotify | Pulp Fiction | [agentbellnorm](http://github.com/agentbellnorm) | morganbentell#9030 | -| Raghunandan Balachandran | Spotify | Pulp Fiction | [soapraj](http://github.com/soapraj) | raghunandanb#1114 | +| Name | Organization | Team | GitHub | Discord | +| ------------------------ | ------------ | ------- | ------------------------------------------------- | ------------------- | +| Alex Lorenzi | Spotify | ProTean | [AlexLorenzi](https://github.com/AlexLorenzi) | `alexlorenzi` | +| Bond Yan | Spotify | ProTean | [byan1197](https://github.com/byan1197) | `bondify` | +| Jackson Chen | Spotify | ProTean | [PeaWarrior](https://github.com/PeaWarrior) | `jacksonc#3322` | +| John Philip | Spotify | ProTean | [johnphilip283](https://github.com/johnphilip283) | `john_philip#2399` | +| Morgan Bentell | Spotify | ProTean | [agentbellnorm](http://github.com/agentbellnorm) | `morganbentell` | +| Raghunandan Balachandran | Spotify | BUX | [soapraj](http://github.com/soapraj) | `raghunandanb#1114` | +| Sydney Achinger | Spotify | ProTean | [squid-ney](https://github.com/squid-ney) | - | ## Incubating Project Areas diff --git a/REVIEWING.md b/REVIEWING.md index 500e15701d..43085090a8 100644 --- a/REVIEWING.md +++ b/REVIEWING.md @@ -20,6 +20,14 @@ When reviewing pull requests it's important to consider our [versioning policy a One other thing to keep in mind, especially when merging pull requests, is where in the release cycle we're currently at. In particular you want to avoid merging any large or risky changes towards the end of each release cycle. If there is a change that is ready to be merged, but you want to hold off until the next main line release, then you can label it with the `merge-after-release` label. +## Configuration + +Pull requests that have [configuration related](https://backstage.io/docs/conf/defining) changes or additions need some extra consideration. + +- Every configuration field consumed by code should have a corresponding `config.d.ts` declaration. +- Carefully consider the `@visibility` of configuration fields in the schema. The default is `backend`. Take extra care that sensitive/secret fields are marked as `@visibility secret` so that they don't accidentally leak. Also ensure that fields that the frontend wants to consume are marked as `@visibility frontend`, otherwise they can't be sent to the browser at all. +- Check that the `config.d.ts` is mentioned in the consuming package's `package.json` as illustrated [in the docs](https://backstage.io/docs/conf/defining). Otherwise it won't get packaged and picked up by the runtime. + ## Changesets We use changesets to track changes in all published packages. Changesets both define what should go into the changelog of each package, but also what kind of version bump should be done for the next release. @@ -52,6 +60,7 @@ Some things that changeset should NOT contain are: - Information related to a different package. - A large amount of content, consider for example a separate migration guide instead, either in the package README or [./docs/](./docs/), and then link to that instead. - Documentation - changesets can describe new features, but it should not be relied on for documenting them. Documentation should either be placed in [TSDoc](https://tsdoc.org) comments, package README, or [./docs/](./docs/). +- Diffs of internal code, for example mirroring what the pull request changes _inside_ a plugin rather than public surfaces. This is not of interest to the reader of a package changelog. Sometimes, however, a small and concise diff can be used in a changeset to illustrate changes that the user will have to make in _their own_ Backstage installation as part of an upgrade, specifically when breaking changes are made to a package. ### When is a changeset needed? diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md index f28a62c018..b88efbb561 100644 --- a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md +++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md @@ -29,7 +29,7 @@ In the AWS console, configure ALB Authentication: - Select `OIDC` under `Authenticate` - Set `Issuer` to `https://login.microsoftonline.com/{TENANT_ID}/v2.0`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. - Set `Authorization endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. -- Set `Token endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. +- Set `Token endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. - Set `User info endpoint` to `https://graph.microsoft.com/oidc/userinfo` - Set `Client ID` to the AAD App `Application (client) Id` - Set `Client secret` to the AAD APP `client secret` diff --git a/docs/accessibility/index.md b/docs/accessibility/index.md index 24685b95ce..b4d63fd4e4 100644 --- a/docs/accessibility/index.md +++ b/docs/accessibility/index.md @@ -15,35 +15,35 @@ There are multiple ways to contribute to making Backstage more accessible, you'l If your plugin lives in the [Backstage main repository](https://github.com/backstage/backstage/) you can modify the [urls in the Lighthouse config](https://github.com/backstage/backstage/blob/39ba2284d73885b7ca8290cb38e2b1e4d983c8d6/lighthouserc.js#L19-L34) to run the Lighthouse checks on urls where your plugin exists as well. E.g. ```diff - ci: { - collect: { - url: [ + ci: { + collect: { + url: [ + /** Software Catalog */ + 'http://localhost:3000/catalog', + 'http://localhost:3000/catalog-import', + 'http://localhost:3000/catalog/default/component/backstage', + ... + /** Your plugin paths */ + 'http://localhost:3000/your-plugin-path, - /** Software Catalog */ - 'http://localhost:3000/catalog', - 'http://localhost:3000/catalog-import', - 'http://localhost:3000/catalog/default/component/backstage', + ], + settings: { ... - ], - settings: { - ... - }, - ... - }, - assert: { + }, ... - }, - }, + }, + assert: { + ... + }, + }, ``` -To make sure the [Accessibility Github workflow](https://github.com/backstage/backstage/blob/master/.github/workflows/verify_accessibility.yml) is running when changes are made to your plugin folders, modify the [list of paths](https://github.com/backstage/backstage/blob/10759b6ad2561bd86183ad940256f9a309c7a6b0/.github/workflows/verify_accessibility.yml#L7-L16). +To make sure the [Accessibility GitHub workflow](https://github.com/backstage/backstage/blob/master/.github/workflows/verify_accessibility.yml) is running when changes are made to your plugin folders, modify the [list of paths](https://github.com/backstage/backstage/blob/10759b6ad2561bd86183ad940256f9a309c7a6b0/.github/workflows/verify_accessibility.yml#L7-L16). ### Run the Lighthouse CLI locally when developing new features If you want to use the Lighthouse CLI and run the checks based on the config you can use the following command: -``` +```shell yarn dlx @lhci/cli@0.11.x autorun ``` diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 644816b053..750e18baf5 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -85,6 +85,8 @@ declare a dependency on the plugin package itself. This is to avoid a direct dependency and potentially cause duplicate installations of the plugin package, while duplicate installations of library packages should always be supported. +To create a Backend module, run `yarn new`, select `backend-module`, and fill out the rest of the prompts. This will create a new package at `plugins/-backend-module-`. + The following is an example of how to create a module that adds a new processor using the `catalogProcessingExtensionPoint`: diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index fdd8cdb1e3..8ede8855e4 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -118,7 +118,7 @@ describe('MyDatabaseClass', () => { // "physical" databases to test against is much costlier than creating the // "logical" databases within them that the individual tests use. const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'], }); // Just an example of how to conveniently bundle up the setup code diff --git a/docs/features/software-catalog/references.md b/docs/features/software-catalog/references.md index 6e0304b2e8..2240d376ce 100644 --- a/docs/features/software-catalog/references.md +++ b/docs/features/software-catalog/references.md @@ -23,9 +23,9 @@ compound reference structure. ## String References -This is the most common alternative, that is used in almost all circumstances. +This is the most common alternative and is used in almost all circumstances. -The string is on the form `[:][/]`, that is, it is +The string is of the form `[:][/]`. That is, it is composed of between one and three parts in this specific order, without any additional encoding: diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md index 5647a6cba4..7553df4081 100644 --- a/docs/integrations/azure/locations.md +++ b/docs/integrations/azure/locations.md @@ -19,10 +19,10 @@ Using a service principal: integrations: azure: - host: dev.azure.com - credential: - clientId: ${CLIENT_ID} - clientSecret: ${CLIENT_SECRET} - tenantId: ${TENANT_ID} + credentials: + - clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} ``` Using a managed identity: @@ -31,8 +31,8 @@ Using a managed identity: integrations: azure: - host: dev.azure.com - credential: - clientId: ${CLIENT_ID} + credentials: + - clientId: ${AZURE_CLIENT_ID} ``` Using a personal access token (PAT): @@ -41,24 +41,54 @@ Using a personal access token (PAT): integrations: azure: - host: dev.azure.com - token: ${AZURE_TOKEN} + credentials: + - personalAccessToken: ${PERSONAL_ACCESS_TOKEN} ``` +You can use specific credentials for different Azure DevOps organizations by specifying the `organizations` field on the credential: + +```yaml +integrations: + azure: + - host: dev.azure.com + credentials: + - organizations: + - my-org + - my-other-org + clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} + - organizations: + - another-org + clientId: ${AZURE_CLIENT_ID} + - organizations: + - yet-another-org + personalAccessToken: ${PERSONAL_ACCESS_TOKEN} +``` + +If you do not specify the `organizations` field the credential will be used for all organizations for which no other credential is configured. + > Note: An Azure DevOps provider is added automatically at startup for > convenience, so you only need to list it if you want to supply a -> [token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate), +> [personalAccessToken](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate), > a [service principal](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity), > or a [managed identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity) The configuration is a structure with these elements: -- `host`: The DevOps host; only `dev.azure.com` is supported. -- `token` (optional): A personal access token as expected by Azure DevOps. -- `credential`: (optional): A service principal or managed identity +- `credentials`: (optional): A service principal, managed identity, or personal access token + +The `credentials` element is a structure with these elements: + +- `organizations`: (optional): A list of organizations for which this credential should be used. If not specified the credential will be used for all organizations for which no other credential is configured. +- `clientId`: The client ID of the service principal or managed identity (required for service principal and managed identities) +- `clientSecret`: The client secret of the service principal (required for service principal) +- `tenantId`: The tenant ID of the service principal (required for service principal) +- `personalAccessToken`: The personal access token (required for personal access token) > Note: > -> - `token` and `credential` are mutually exclusive. > - You cannot use a service principal or managed identity for Azure DevOps Server (on-premises) organizations > - You can only use a service principal or managed identity for Azure AD backed Azure DevOps organizations -> - The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API +> - You can only specify one credential per host without any organizations specified +> - The personal access token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API diff --git a/docs/plugins/integrating-plugin-into-software-catalog.md b/docs/plugins/integrating-plugin-into-software-catalog.md index a44e6d01c0..1ec3df69cf 100644 --- a/docs/plugins/integrating-plugin-into-software-catalog.md +++ b/docs/plugins/integrating-plugin-into-software-catalog.md @@ -37,7 +37,7 @@ You can access the currently selected entity using the backstage api import { useEntity } from '@backstage/plugin-catalog-react'; export const MyPluginEntityContent = () => { - const { entity, loading, error, refresh } = useEntity(); + const entity = useEntity(); // Do something with the entity data... }; diff --git a/docs/publishing.md b/docs/publishing.md index 12e4878d5a..b2224e76b7 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -18,6 +18,22 @@ PR is merged. This is typically done every Tuesday around noon CET. **This emergency release process is intended only for the Backstage maintainers.** +Given one or more PRs towards master that we want to create a patch release for, run the following script from the repo root: + +```bash +./scripts/patch-release-for-pr.js ... +``` + +Wait until the script has finished executing, at the end of the output you will find a link of the format `https://github.com/backstage/backstage/compare/patch/...`. Open this link in your browser to create a PR for the patch release. Finish the sentence "This release fixes an issue where..." and create the PR. + +Once the PR has been approved and merged, the patch release will be automatically created. The patch release is complete when a notification has been posted to Discord in the `#announcements` channel. Keep an eye on "Deploy Packages" workflow and re-trigger if it fails. It is safe to re-trigger any part of this workflow, including the release step. + +If the above process fails, you can fall back to the manual process documented below. + +### Old Process + +This is the old and manual process that we used before the patch script, provided here as a reference: + For this example we will be using the `@backstage/plugin-foo` package as an example and assume that it is currently version `6.5.0` in the master branch. diff --git a/docs/releases/v1.18.0-next.0-changelog.md b/docs/releases/v1.18.0-next.0-changelog.md new file mode 100644 index 0000000000..16c71179e5 --- /dev/null +++ b/docs/releases/v1.18.0-next.0-changelog.md @@ -0,0 +1,2753 @@ +# Release v1.18.0-next.0 + +## @backstage/config-loader@1.5.0-next.0 + +### Minor Changes + +- 9606ba0939e6: Deep visibility now also applies to values that are not covered by the configuration schema. + + For example, given the following configuration schema: + + ```ts + // plugins/a/config.schema.ts + export interface Config { + /** @deepVisibility frontend */ + a?: unknown; + } + + // plugins/a/config.schema.ts + export interface Config { + a?: { + b?: string; + }; + } + ``` + + All values under `a` are now visible to the frontend, while previously only `a` and `a/b` would've been visible. + +### Patch Changes + +- f9657b891b00: Do not unnecessarily notify subscribers when no-op updates to config happen +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/core-app-api@1.10.0-next.0 + +### Minor Changes + +- 18619f793c94: Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. +- 18619f793c94: The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/core-plugin-api@1.6.0-next.0 + +### Minor Changes + +- 18619f793c94: Added the optional `expiresAt` field that may now be part of a `BackstageIdentityResponse`. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/integration@1.7.0-next.0 + +### Minor Changes + +- 5f1a92b9f19f: Added `AzureDevOpsCredentialsProvider` to support multiple Azure DevOps organizations and **deprecated** `AzureIntegrationConfig.credential` and `AzureIntegrationConfig.token` in favour of `AzureIntegrationConfig.credentials`. You can now use specific credentials for different Azure DevOps (Server) organizations by specifying the `organizations` field on a credential: + + ```yaml + integrations: + azure: + - host: dev.azure.com + credentials: + - organizations: + - my-org + - my-other-org + clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} + - organizations: + - yet-another-org + personalAccessToken: ${PERSONAL_ACCESS_TOKEN} + ``` + + See the [Azure integration documentation](https://backstage.io/docs/integrations/azure/locations) for more information. + +### Patch Changes + +- cb2e19d82d95: Gitiles: Fixed auth prefix issue +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: New module for `@backstage/plugin-auth-backend` that adds a GCP IAP auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.0 + +### Minor Changes + +- 23af27f5ce79: New module for `@backstage/plugin-auth-backend` that adds a GitHub auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: New module for `@backstage/plugin-auth-backend` that adds a Google auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + +## @backstage/plugin-auth-node@0.3.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: Introduced a new system for building auth providers for `@backstage/plugin-auth-backend`, which both increases the amount of code re-use across providers, and also works better with the new backend system. + + Many existing types have been moved from `@backstage/plugin-auth-backend` in order to avoid a direct dependency on the plugin from modules. + + Auth provider integrations are now primarily implemented through a pattern of creating "authenticators", which are in turn specific to each kind of integrations. Initially there are two types: `createOAuthAuthenticator` and `createProxyAuthenticator`. These come paired with functions that let you create the corresponding route handlers, `createOAuthRouteHandlers` and `createProxyAuthRouteHandlers`, as well as provider factories, `createOAuthProviderFactory` and `createProxyAuthProviderFactory`. This new authenticator pattern allows the sign-in logic to be separated from the auth integration logic, allowing it to be completely re-used across all providers of the same kind. + + The new provider factories also implement a new declarative way to configure sign-in resolvers, rather than configuration through code. Sign-in resolvers can now be configured through the `resolvers` configuration key, where the first resolver that provides an identity will be used, for example: + + ```yaml + auth: + providers: + google: + development: + clientId: ... + clientSecret: ... + signIn: + resolvers: + - resolver: emailMatchingUserEntityAnnotation + - resolver: emailLocalPartMatchingUserEntityName + ``` + + These configurable resolvers are created with a new `createSignInResolverFactory` function, which creates a sign-in resolver factory, optionally with an options schema that will be used both when configuring the sign-in resolver through configuration and code. + + The internal helpers from `@backstage/plugin-auth-backend` that were used to implement auth providers using passport strategies have now also been made available as public API, through `PassportHelpers` and `PassportOAuthAuthenticatorHelper`. + +### Patch Changes + +- 18619f793c94: The `BackstageIdentityResponse` interface now has an optional `expiresInSeconds` field that can be used to signal session expiration. The `prepareBackstageIdentityResponse` utility will now also read the expiration from the provided token, and include it in the response. +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-opencost@0.2.0-next.0 + +### Minor Changes + +- 1380a689ab56: New OpenCost plugin provides an port of the latest OpenCost UI to Backstage with updated dependencies. The plugin's README covers installation and configuration + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/app-defaults@1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## @backstage/backend-app-api@0.5.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## @backstage/backend-common@0.19.4-next.0 + +### Patch Changes + +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-dev-utils@0.1.1 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/types@1.1.0 + +## @backstage/backend-defaults@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + +## @backstage/backend-plugin-api@0.6.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/backend-tasks@0.5.7-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/backend-test-utils@0.2.2-next.0 + +### Patch Changes + +- 9fb3b5373c45: Extended `mockService` to also include mocked variants, for example `mockServices.lifecycle.mock()`. The returned mocked implementation will have a `factory` property which is a service factory for itself. You can also pass a partial implementation of the service to the mock function to use a mock implementation of specific methods. +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## @backstage/cli@0.22.12-next.0 + +### Patch Changes + +- 278d9326eb40: Added the ability to create a plain backend module with the `new` command. +- 71d4368ae5cc: Added support for the `dev/index` entry point for backend plugins and modules. +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/eslint-plugin@0.1.3 + - @backstage/release-manifests@0.0.9 + - @backstage/types@1.1.0 + +## @backstage/core-components@0.13.5-next.0 + +### Patch Changes + +- 47782f4bfa5b: Add loading indicator to Table +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/version-bridge@1.0.4 + +## @backstage/create-app@0.5.5-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.12 + +## @backstage/dev-utils@1.0.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/integration-react@1.1.18-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/repo-tools@0.3.4-next.0 + +### Patch Changes + +- 0109d3f11159: The `generate-catalog-info` command now uses the first listed `CODEOWNER` as Component owner when initially + creating the `catalog-info.yaml` file. It continues to allow any one listed `CODEOWNER` when updating + entity metadata. +- ec13d3e86028: Fixed a bug with the `generate-catalog-info` command that could cause the `--dry-run` flag to indicate changes to files when no changes would actually be made if the command were run without the flag. +- db60a16e0a54: Added a `--ci` flag to the `generate-catalog-info` command. This flag behaves similarly to the same flag on `api-reports`: if `catalog-info.yaml` files would have been added or modified, then the process exits with status code `1`, and instructions are printed. +- Updated dependencies + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + +## @techdocs/cli@1.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + +## @backstage/test-utils@1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## @backstage/plugin-adr@0.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-adr-common@0.2.14-next.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-adr-backend@0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-adr-common@0.2.14-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-adr-common@0.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-airbrake@0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/dev-utils@1.0.20-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-airbrake-backend@0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-allure@0.1.39-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-analytics-module-ga@0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-ga4@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-newrelic-browser@0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-apache-airflow@0.2.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + +## @backstage/plugin-api-docs@0.9.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-api-docs-module-protoc-gen-doc@0.1.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency + +## @backstage/plugin-apollo-explorer@0.1.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-app-backend@0.3.50-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-app-node@0.1.2-next.0 + +## @backstage/plugin-app-node@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + +## @backstage/plugin-auth-backend@0.18.8-next.0 + +### Patch Changes + +- 7944d43f4790: Added `authPlugin` export for the new backend system. The plugin does not include any built-in auth providers, they must instead be added by installing additional modules, for example `authModuleGoogleProvider` from `@backstage/plugin-auth-backend-module-google-provider`. +- 8513cd7d00e3: Deprecated several exports that are now available from `@backstage/plugin-auth-node` instead. +- 7944d43f4790: Added the ability to disable the built-in auth providers by passing `disableDefaultProviderFactories` to `createRouter`. +- 7944d43f4790: The algorithm used when generating Backstage tokens can be configured via `auth.identityTokenAlgorithm`. +- Updated dependencies + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-azure-devops@0.3.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-devops-common@0.3.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-azure-devops-backend@0.3.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-azure-sites-backend@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.47-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-badges-backend@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-bazaar@0.2.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-bazaar-backend@0.2.13-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-bitbucket-cloud-common@0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + +## @backstage/plugin-bitrise@0.1.50-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-catalog@1.12.4-next.0 + +### Patch Changes + +- 832eef72485b: Added title to props for `HasCards`. +- 163a41035e42: Fixed an issue where `EntitySwitch` was preventing the display of entity errors. +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-catalog-backend@1.12.2-next.0 + +### Patch Changes + +- 149361e81622: Fix to the `limit` parameter on entity queries. +- 0198aa596fd9: Fixed a link to the frontend Backstage plugin that had pointed to itself. +- 41d1b2d628ea: Fix OpenAPI schema for the facets endpoint +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-search-backend-module-catalog@0.1.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-catalog-backend-module-aws@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + +## @backstage/plugin-catalog-backend-module-azure@0.1.21-next.0 + +### Patch Changes + +- 94f96508491d: Improve consistency of results from the `AzureDevOpsEntityProvider`. +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- 044b4f2fb1e3: Remove duplications from Azure search before committing the new locations to the catalog. +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.11-next.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-bitbucket-cloud-common@0.2.11-next.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + +## @backstage/plugin-catalog-graph@0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-catalog-import@0.9.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-catalog-node@1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-catalog-react@1.8.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-cicd-statistics@0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-cicd-statistics@0.1.25-next.0 + +## @backstage/plugin-circleci@0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-cloudbuild@0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-code-climate@0.1.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-code-coverage@0.2.16-next.0 + +### Patch Changes + +- 1d8f4f0a7486: Use fetchApi to ensure authorization is used when fetching code-coverage data +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-code-coverage-backend@0.2.16-next.0 + +### Patch Changes + +- 3f1367b956c6: Added support for LCOV coverage reports +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-codescene@0.1.17-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-config-schema@0.1.45-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-cost-insights@0.12.12-next.0 + +### Patch Changes + +- a54cd411639e: Updated dependency `@types/pluralize` to `^0.0.30`. +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-devtools@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-devtools-common@0.1.3 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## @backstage/plugin-devtools-backend@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-devtools-common@0.1.3 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## @backstage/plugin-dynatrace@7.0.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-entity-feedback@0.2.6-next.0 + +### Patch Changes + +- 5e2e06db747a: Improve README to note that Backstage identity is required to be configured +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-entity-feedback-common@0.1.2 + +## @backstage/plugin-entity-feedback-backend@0.1.8-next.0 + +### Patch Changes + +- 740155e8e976: Improve backend logging if method calls fail +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-entity-feedback-common@0.1.2 + +## @backstage/plugin-entity-validation@0.1.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-events-backend@0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-azure@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-github@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-gitlab@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-test-utils@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-node@0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + +## @backstage/plugin-explore@0.4.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.31-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-explore-backend@0.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-backend-module-explore@0.1.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-explore-react@0.0.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/plugin-explore-common@0.0.1 + +## @backstage/plugin-firehydrant@0.2.7-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-fossa@0.2.55-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-gcalendar@0.3.18-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gcp-projects@0.3.41-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-git-release-manager@0.3.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-actions@0.6.4-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-github-deployments@0.1.54-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-github-issues@0.2.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-github-pull-requests-board@0.1.17-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-gitops-profiles@0.3.40-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gocd@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-graphiql@0.2.54-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-graphql-backend@0.1.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-graphql@0.3.22 + +## @backstage/plugin-graphql-voyager@0.1.7-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-home@0.5.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home-react@0.1.3-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-home-react@0.1.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + +## @backstage/plugin-ilert@0.2.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-jenkins@0.8.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-jenkins-common@0.1.18 + +## @backstage/plugin-jenkins-backend@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-jenkins-common@0.1.18 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## @backstage/plugin-kafka@0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-kafka-backend@0.2.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-kubernetes@0.10.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + +## @backstage/plugin-kubernetes-backend@0.11.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## @backstage/plugin-lighthouse@0.4.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-lighthouse-common@0.1.2 + +## @backstage/plugin-lighthouse-backend@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-lighthouse-common@0.1.2 + +## @backstage/plugin-linguist@0.1.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-linguist-common@0.1.1 + +## @backstage/plugin-linguist-backend@0.4.2-next.0 + +### Patch Changes + +- b2de501bda31: When creating the router using `createRouterFromConfig` or using the plugin for the new backend system the `linguist.useSourceLocation` configuration is now optional. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-linguist-common@0.1.1 + +## @backstage/plugin-microsoft-calendar@0.1.7-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-newrelic@0.3.40-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-newrelic-dashboard@0.2.16-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-nomad@0.1.4-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-nomad-backend@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-octopus-deploy@0.2.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-org@0.6.13-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-org-react@0.1.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-pagerduty@0.6.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home-react@0.1.3-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-periskop@0.1.21-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-periskop-backend@0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-permission-backend@0.5.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## @backstage/plugin-permission-node@0.7.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-permission-react@0.4.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-playlist@0.1.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-playlist-common@0.1.9 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-playlist-backend@0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-playlist-common@0.1.9 + +## @backstage/plugin-proxy-backend@0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-puppetdb@0.1.6-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-rollbar@0.4.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-rollbar-backend@0.1.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-scaffolder@1.14.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/plugin-scaffolder-react@1.5.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + +## @backstage/plugin-scaffolder-backend@1.16.3-next.0 + +### Patch Changes + +- a4989552d828: Add examples for `publish:github` and `publish:gitlab` scaffolder actions. +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- fb57a4694fc6: Fixed the plugin and module ID of the alpha `catalogModuleTemplateKind` export. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-node@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-common@1.4.0 + +## @backstage/plugin-scaffolder-react@1.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + +## @backstage/plugin-search@1.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-search-backend@1.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-catalog@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-elasticsearch@1.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/integration-aws-node@0.1.5 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-explore@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-pg@0.5.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-techdocs@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + +## @backstage/plugin-search-backend-node@1.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-react@1.6.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-sentry@0.5.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-shortcuts@0.3.14-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-sonarqube@0.7.4-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-sonarqube-react@0.1.8-next.0 + +## @backstage/plugin-sonarqube-backend@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-sonarqube-react@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/catalog-model@1.4.1 + +## @backstage/plugin-splunk-on-call@0.4.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-stack-overflow@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home-react@0.1.3-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-stack-overflow-backend@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-stackstorm@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-tech-insights@0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-backend@0.5.16-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + - @backstage/plugin-tech-insights-node@0.4.8-next.0 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-tech-insights-common@0.2.11 + - @backstage/plugin-tech-insights-node@0.4.8-next.0 + +## @backstage/plugin-tech-insights-node@0.4.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-radar@0.6.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-techdocs@1.6.8-next.0 + +### Patch Changes + +- 88c9525a36f3: Fixed bug in styles that caused next and previous links in footer to overlap page content. +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/plugin-techdocs@1.6.8-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-react@1.6.5-next.0 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + +## @backstage/plugin-techdocs-backend@1.6.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-backend-module-techdocs@0.1.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + +## @backstage/plugin-techdocs-node@1.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-techdocs-react@1.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/version-bridge@1.0.4 + +## @backstage/plugin-todo@0.2.25-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-todo-backend@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-user-settings@0.7.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-user-settings-backend@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-vault@0.1.17-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-vault-backend@0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-xcmetrics@0.2.42-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## example-app@0.2.87-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.0 + - @backstage/integration-react@1.1.18-next.0 + - @backstage/plugin-microsoft-calendar@0.1.7-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.16-next.0 + - @backstage/plugin-apache-airflow@0.2.15-next.0 + - @backstage/plugin-github-actions@0.6.4-next.0 + - @backstage/plugin-octopus-deploy@0.2.5-next.0 + - @backstage/plugin-azure-devops@0.3.5-next.0 + - @backstage/plugin-gcp-projects@0.3.41-next.0 + - @backstage/plugin-azure-sites@0.1.12-next.0 + - @backstage/plugin-cloudbuild@0.3.23-next.0 + - @backstage/plugin-lighthouse@0.4.8-next.0 + - @backstage/plugin-tech-radar@0.6.8-next.0 + - @backstage/plugin-dynatrace@7.0.3-next.0 + - @backstage/plugin-gcalendar@0.3.18-next.0 + - @backstage/plugin-shortcuts@0.3.14-next.0 + - @backstage/plugin-airbrake@0.3.23-next.0 + - @backstage/plugin-circleci@0.3.23-next.0 + - @backstage/plugin-graphiql@0.2.54-next.0 + - @backstage/plugin-linguist@0.1.8-next.0 + - @backstage/plugin-newrelic@0.3.40-next.0 + - @backstage/plugin-playlist@0.1.15-next.0 + - @backstage/plugin-puppetdb@0.1.6-next.0 + - @backstage/plugin-jenkins@0.8.5-next.0 + - @backstage/plugin-rollbar@0.4.23-next.0 + - @backstage/plugin-badges@0.2.47-next.0 + - @backstage/plugin-sentry@0.5.8-next.0 + - @backstage/plugin-kafka@0.3.23-next.0 + - @backstage/plugin-nomad@0.1.4-next.0 + - @backstage/plugin-todo@0.2.25-next.0 + - @backstage/plugin-org@0.6.13-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/plugin-techdocs@1.6.8-next.0 + - @backstage/plugin-entity-feedback@0.2.6-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/plugin-code-coverage@0.2.16-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/plugin-cost-insights@0.12.12-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/plugin-adr@0.6.6-next.0 + - @backstage/plugin-catalog-import@0.9.13-next.0 + - @backstage/plugin-scaffolder@1.14.4-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.18-next.0 + - @backstage/plugin-home@0.5.7-next.0 + - @backstage/plugin-pagerduty@0.6.4-next.0 + - @backstage/plugin-stack-overflow@0.1.20-next.0 + - @backstage/plugin-api-docs@0.9.10-next.0 + - @internal/plugin-catalog-customized@0.0.14-next.0 + - @backstage/plugin-catalog-graph@0.2.35-next.0 + - @backstage/plugin-explore@0.4.9-next.0 + - @backstage/plugin-scaffolder-react@1.5.4-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-devtools@0.1.4-next.0 + - @backstage/plugin-gocd@0.1.29-next.0 + - @backstage/plugin-kubernetes@0.10.2-next.0 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-search@1.3.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + - @backstage/plugin-stackstorm@0.1.6-next.0 + - @backstage/plugin-tech-insights@0.3.15-next.0 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + - @backstage/plugin-user-settings@0.7.8-next.0 + +## example-backend@0.2.87-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-auth-backend@0.18.8-next.0 + - @backstage/plugin-code-coverage-backend@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend@1.16.3-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/plugin-linguist-backend@0.4.2-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/plugin-entity-feedback-backend@0.1.8-next.0 + - @backstage/plugin-tech-insights-backend@0.5.16-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/plugin-app-backend@0.3.50-next.0 + - example-app@0.2.87-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-adr-backend@0.3.8-next.0 + - @backstage/plugin-azure-devops-backend@0.3.29-next.0 + - @backstage/plugin-azure-sites-backend@0.1.12-next.0 + - @backstage/plugin-badges-backend@0.2.5-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.2-next.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-devtools-backend@0.1.5-next.0 + - @backstage/plugin-events-backend@0.2.11-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-explore-backend@0.0.12-next.0 + - @backstage/plugin-graphql-backend@0.1.40-next.0 + - @backstage/plugin-jenkins-backend@0.2.5-next.0 + - @backstage/plugin-kafka-backend@0.2.43-next.0 + - @backstage/plugin-kubernetes-backend@0.11.5-next.0 + - @backstage/plugin-lighthouse-backend@0.2.6-next.0 + - @backstage/plugin-nomad-backend@0.1.4-next.0 + - @backstage/plugin-permission-backend@0.5.25-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-playlist-backend@0.3.6-next.0 + - @backstage/plugin-proxy-backend@0.3.2-next.0 + - @backstage/plugin-rollbar-backend@0.1.47-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.3-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.19-next.0 + - @backstage/plugin-search-backend@1.4.2-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.5-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.11-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.34-next.0 + - @backstage/plugin-tech-insights-node@0.4.8-next.0 + - @backstage/plugin-techdocs-backend@1.6.7-next.0 + - @backstage/plugin-todo-backend@0.2.2-next.0 + +## example-backend-next@0.0.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-scaffolder-backend@1.16.3-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/plugin-linguist-backend@0.4.2-next.0 + - @backstage/plugin-entity-feedback-backend@0.1.8-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/plugin-app-backend@0.3.50-next.0 + - @backstage/backend-defaults@0.2.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-adr-backend@0.3.8-next.0 + - @backstage/plugin-azure-devops-backend@0.3.29-next.0 + - @backstage/plugin-badges-backend@0.2.5-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.2-next.0 + - @backstage/plugin-devtools-backend@0.1.5-next.0 + - @backstage/plugin-kubernetes-backend@0.11.5-next.0 + - @backstage/plugin-lighthouse-backend@0.2.6-next.0 + - @backstage/plugin-permission-backend@0.5.25-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-proxy-backend@0.3.2-next.0 + - @backstage/plugin-search-backend@1.4.2-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.6-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.6-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.6-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-techdocs-backend@1.6.7-next.0 + - @backstage/plugin-todo-backend@0.2.2-next.0 + +## e2e-test@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.5-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + +## techdocs-cli-embedded-app@0.2.86-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/plugin-techdocs@1.6.8-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + +## @internal/plugin-catalog-customized@0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @internal/plugin-todo-list@1.0.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @internal/plugin-todo-list-backend@1.0.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 diff --git a/lighthouserc.js b/lighthouserc.js index 90a15abbd3..70136ee158 100644 --- a/lighthouserc.js +++ b/lighthouserc.js @@ -32,6 +32,17 @@ module.exports = { 'http://localhost:3000/create/templates/default/react-ssr-template', /** Search */ 'http://localhost:3000/search', + /** Miscellaneous */ + 'http://localhost:3000/settings', + /** plugin-devtools */ + 'http://localhost:3000/devtools', + 'http://localhost:3000/devtools/config', + /** plugin-explore */ + 'http://localhost:3000/explore', + 'http://localhost:3000/explore/groups', + 'http://localhost:3000/explore/tools', + /** plugin-tech-radar */ + 'http://localhost:3000/tech-radar', ], settings: { onlyCategories: ['accessibility'], diff --git a/microsite/blog/2023-08-17-expedia-proof-of-value-metrics-2.mdx b/microsite/blog/2023-08-17-expedia-proof-of-value-metrics-2.mdx new file mode 100644 index 0000000000..9c6e67fcf1 --- /dev/null +++ b/microsite/blog/2023-08-17-expedia-proof-of-value-metrics-2.mdx @@ -0,0 +1,206 @@ +--- +# prettier-ignore +title: "Expedia Group shares Backstage proof of value metrics 2.0" +author: Guillermo Manzo, Expedia Group +authorURL: https://www.linkedin.com/in/guillermomanzo +--- + +![backstage header](assets/2023-08-17/header.png) + +_Editor's Note: In this post, Expedia Group provides a follow-up to their initial Q4 2021 [post on how they measure Backstage proof of value](https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/). +We're grateful to the Expedia Group team for sharing their processes, revised proof of value metrics, and adaptations to their approach over time in hopes of providing a successful framework for the rest of the Backstage community._ + +[Expedia Group's](https://expediagroup.com) mission is to power global travel for everyone, everywhere. +This vision is made possible through Expedia Group's massive platform, which connects 168 million loyalty members, 50 thousand B2B partners, 3 million properties and 500 airlines, car rentals and cruise lines across the globe. In short, the scale of our platform is enormous, which means that we need to scale our engineering capabilities as well so we can continue to provide seamless experiences to our travelers and partners. + +{/* truncate */} + +In August 2020, we adopted Backstage to enhance our foundational platform capabilities and reduce developer friction in the process. Its framework proved scalable for us and as EG's platform evolved, so did our application of Backstage. Since our [initial share about our Backstage deployment](https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/), we've continued to follow our process of: + +- Syncing Expedia Group's internal Developer Journey phases by key DevOps processes and Software Delivery Life Cycle (SDLC) milestones (POC, Alpha, Beta, etc). +- Identifying Developer Jobs to be Done (JTBD) by our Internal Developer Journey phases +- Aligning developer JTBD's to proof of value (PoV) metrics. + +In the diagram below, you can see how we distilled these processes into Backstage PoV metrics. + +![backstage pov metrics](assets/2023-08-17/table.png) + +To ground ourselves in the evolving developer and Backstage landscape at Expedia Group, we constantly ask ourselves, "How can we continue to show the value of Backstage to our customers and the Developer Community?" To fully answer this question, we adapted our initial proof of value metrics to reinforce our alignment to the current landscape of the Expedia Group developer journey. + +## Expedia Group proof of value metrics 2.0 + +We are excited to share our PoV metrics because it affirms our investment in Backstage for Expedia Group's Backstage users and provides a compelling metrics-based answer on why more organizations should consider adopting it. + +As our Backstage application matured, we updated our PoV metrics and aligned our 2023 roadmap to reflect these changes. Below, we highlight key wins from 2022 and outline our long-term vision for each metric. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PoV MetricDescription2022 Key Win(s)Long-Term Vision
"Ship on Day One" / onboarding metrics +

+ A metric used to determine an engineer's readiness to develop at EG + during their onboarding process. +

+

+ It measures the time from first commit to deploying code in + production, using a "Hello World Template" +

+
+
    +
  • + 850+ engineers went through the “Ship on Day One” Bootcamp when + first joining Expedia Group +
  • +
  • + We had a success rate of ~95% of engineers completing bootcamp on + first try +
  • +
  • 7,000+ components created or onboarded
  • +
  • 4-min average per app creation/onboarding
  • +
+
+
    +
  • + Develop full onboarding curriculum coverage for all critical + capabilities offered within EG - Example: performance and quality + management, pipeline management, etc. +
  • +
  • + Create an end-to-end guided tour of the EG Developer Journey w/ + functional training +
  • +
+
+ Reduce context switching through integration with core EG capabilities + + Through plugins, tool integration, reduces context-switching as + engineers will not need to toggle between multiple tools + +
    +
  • + 15%+ of internal developer tools are integrated within Backstage, + reducing the amount of context-switching needed +
  • +
  • + 4000+ unique users using Backstage for ~20 min on average a day. +
  • +
+
+
    +
  • + End-to-end integration with EG paved road tools and Backstage to + support increased developer productivity +
  • +
  • + Drive broader adoption to have all EG employees using Backstage to + manage their SDLC and to build internal UIs using plugins +
  • +
+
Improve search results relevance + Intuitive search capabilities to surface relevant Backstage content like + TechDocs, entities, tools, etc. + +
    +
  • Enabled global search functionality throughout Backstage
  • +
  • +

    Contributed to the open source Backstage Search platform

    +
  • +
+
+
    +
  • Engine optimization/ranking improvement
  • +
  • Visualize key search metrics to support improvements
  • +
  • Chat + AI integration with chat-based interfaces
  • +
+
Technical documentation reach + Number of developers leveraging TechDocs for all services within EG + +
    +
  • 500+ services tied with TechDocs
  • +
  • 50,000+ average monthly TechDoc views
  • +
+
+
    +
  • + All Backstage entities leveraging TechDocs to enable a centralized + documentation portal integrated with the Software Catalog +
  • +
  • + Quality/governance and gamification to support users and + contributors +
  • +
  • + More TechDocs and mkdocs add-ons to compliment the core capabilities + and drive usability of documentation on Backstage +
  • +
+
Developer Experience community engagementEnhance Backstage through community engagement +
    +
  • + Diverse contributions to Backstage open source focused on search and + discovery - 10+ +
  • +
  • Community engagement through internal case studies, and blogs
  • +
+
+
    +
  • + Continuous contributions to open-source Backstage, focused on plugin + administration, driving documentation adoption, and improved UX + around core capabilities +
  • +
  • + Strategic community engagements to enhance the platform's usability + and long-term confidence in adopting Backstage +
  • +
  • + Event participation and storytelling around EG's Backstage journey +
  • +
+
+ +## YMMV: Your Metrics May Vary! + +Measuring the value of Backstage will vary depending on your organization's needs and priorities. As an early adopter of Backstage, we hope the Expedia Group PoV metrics provide a helpful framework for understanding how Backstage can be used to improve developer productivity, collaboration, and knowledge sharing. + +We look forward to sharing future updates with the community as we continue to embrace a continuous improvement approach to Backstage. If you missed it, you can read about where we started in our [initial proof of value metrics](https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/) back in 2021. + +_Read more about Expedia's tech journey on [their Medium Blog](https://medium.com/expedia-group-tech)_ diff --git a/microsite/blog/assets/2023-08-17/header.png b/microsite/blog/assets/2023-08-17/header.png new file mode 100644 index 0000000000..bd75529f72 Binary files /dev/null and b/microsite/blog/assets/2023-08-17/header.png differ diff --git a/microsite/blog/assets/2023-08-17/table.png b/microsite/blog/assets/2023-08-17/table.png new file mode 100644 index 0000000000..c832740634 Binary files /dev/null and b/microsite/blog/assets/2023-08-17/table.png differ diff --git a/microsite/data/plugins/solo-io-gloo-platform-portal.yaml b/microsite/data/plugins/solo-io-gloo-platform-portal.yaml index fe7f4d78da..2f17b84f17 100644 --- a/microsite/data/plugins/solo-io-gloo-platform-portal.yaml +++ b/microsite/data/plugins/solo-io-gloo-platform-portal.yaml @@ -4,7 +4,7 @@ author: Solo.io authorUrl: https://solo.io category: Infrastructure description: View, test, and manage your Gloo Platform Portal APIs through our Backstage plugin. -documentation: https://github.com/solo-io/dev-portal-backstage-public#readme +documentation: https://github.com/solo-io/backstage-plugins-overview#readme iconUrl: /img/solo-io-glooy-circle.png.webp npmPackageName: '@solo.io/dev-portal-backstage-plugin' addedDate: '2023-05-19' diff --git a/package.json b/package.json index 72442168d6..24f0d415de 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@types/react": "^17", "@types/react-dom": "^17" }, - "version": "1.17.0", + "version": "1.18.0-next.0", "dependencies": { "@backstage/errors": "workspace:^", "@manypkg/get-packages": "^1.1.3" diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 8d3a232e43..260972bcf0 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/app-defaults +## 1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-permission-react@0.4.15-next.0 + ## 1.4.2 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 4edca8c3f3..3fd157b74a 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "1.4.2", + "version": "1.4.3-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/app-next/.eslintrc.js b/packages/app-next/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/app-next/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/app-next/README.md b/packages/app-next/README.md new file mode 100644 index 0000000000..21645609a9 --- /dev/null +++ b/packages/app-next/README.md @@ -0,0 +1,3 @@ +# example-app-next + +This package is an example of a Backstage application. diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml new file mode 100644 index 0000000000..463113fd55 --- /dev/null +++ b/packages/app-next/app-config.yaml @@ -0,0 +1,108 @@ +app: + packages: 'all' # ✨ + extensions: + - core.router + - graphiql.page: + config: + path: / + + # - core.signInPage: + # props: + # provider: + # id: google + # title: Google + # message: Sign In using Google + # apiRef: googleAuthApiRef # ??? + + # - core.nav: + # config: + # logo: assets/logo.png + # layout: + # - label: Search + # icon: search + # to: /search + # items: + # - point: search + # - type: divider + # - label: Menu + # icon: menu + # items: + # - label: Home + # icon: home + # to: /catalog + # - label: Create... + # icon: create + # to: /create + # - type: divider + # - type: scroll-wrapper + # items: + # - label: Tech Radar + # icon: map + # to: /tech-radar + # - label: GraphiQL + # icon: graphiql + # to: /graphiql + # - type: divider + # - point: shortcuts + # - type: space + # - type: divider + # - label: Settings + # icon: avatar + # to: /settings + # items: + # - point: settings + # - core.nav/search: '@backstage/plugin-search#SidebarSearchModal' + # - core.nav/shortcuts: + # use: '@backstage/plugin-shortcuts#Shortcuts' + # config: + # allowExternalLinks: true + # - core.nav/settings: '@backstage/plugin-user-settings#SidebarSettings' + + # - core.pages.index: + # at: + # point: core.routes + # config: + # path: / + # use: 'react-router-dom#Navigate' + # config: + # to: /catalog + + # - scaffolder.page: + # config: + # groups: + # - title: Recommended + # filter: + # entity.metadata.tags: recommended + # - scaffolder.page/fields: '@backstage/plugin-scaffolder#LowerCaseValuePickerFieldExtension' + # - scaffolder.page/layout: '@backstage/plugin-scaffolder#TwoColumnLayout' + + # - search.page/content: 'app#customSearchPage' # custom search page from somewhere + + # - user-settings.page.routes.advanced: + # at: + # point: user-settings.page/routes + # config: + # title: Advanced + # path: /advanced + # use: '@backstage/plugin-user-settings#AdvancedSettings' + + # - entity.card.orphanWarning + # - entity.card.processingErrors + # - entity.card.about + # - entity.card.catalogGraph + # - entity.card.pagerDuty + # - entity.card.links + # - entity.card.labels + # - entity.card.githubInsightsLanguages + # - entity.card.githubInsightsReleases + # - entity.card.githubInsightsReadme: + # config: + # maxHeight: 350 # Throwing this config in to have an example, but in practice rely on default + # - entity.card.subcomponentsCard + # - entity.card.userProfile + # - entity.card.ownership + # - entity.card.likeDislikeRatings + # - entity.content.dependsOnComponents + # - entity.content.codeInsights + # - entity.content.todo + # - entity.content.feedbackResponse diff --git a/packages/app-next/architecture.drawio.svg b/packages/app-next/architecture.drawio.svg new file mode 100644 index 0000000000..b5ed4de71a --- /dev/null +++ b/packages/app-next/architecture.drawio.svg @@ -0,0 +1,614 @@ + + + + + + + + + + + + + + + + + + +
+
+
+ Extension Instance +
+
+
+
+ + Extension Ins... + +
+
+ + + + + + + +
+
+
+ Extension +
+
+
+
+ + Extension + +
+
+ + + + +
+
+
+ Output +
+
+
+
+ + Output + +
+
+ + + + + +
+
+
+ Input 1 +
+
+
+
+ + Input 1 + +
+
+ + + + + +
+
+
+ Input 2 +
+
+
+
+ + Input 2 + +
+
+ + + + + +
+
+
+ React Component +
+
+
+
+ + React Component + +
+
+ + + + + +
+
+
+ Route Path +
+
+
+
+ + Route Path + +
+
+ + + + + +
+
+
+ Display Title +
+
+
+
+ + Display Title + +
+
+ + + + + + +
+
+
+ Configuration +
+
+
+
+ + Configuration + +
+
+ + + + + + +
+
+
+ Parent +
+
+
+
+ + Parent + +
+
+ + + + + +
+
+
+ + Configuration Schema + +
+
+
+
+ + Configuration Sc... + +
+
+ + + + +
+
+
+ ID +
+
+
+
+ + ID + +
+
+ + + + + +
+
+
+ Multi Input Attachments +
+
+
+
+ + Multi Input Attachments + +
+
+ + + + +
+
+
+ Child +
+
+
+
+ + Child + +
+
+ + + + + + +
+
+
+ Child +
+
+
+
+ + Child + +
+
+ + + + + + + + + + + + + + +
+
+
+ Single Input Attachment +
+
+
+
+ + Single Input Attachm... + +
+
+ + + + +
+
+
+ Child +
+
+
+
+ + Child + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Other +
+
+
+
+ + Other + +
+
+ + + + + + + + + + + + + + + +
+
+
+ Output Data +
+
+
+
+ + Output Data + +
+
+ + + + + +
+
+
+ Input Data +
+
+
+
+ + Input Data + +
+
+ + + + + + + + + +
+
+
+ Root +
+
+
+
+ + Root + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Example Extension Data Types + +
+
+
+
+ + Example Extension Data Types + +
+
+ + + + +
+
+
+ Extension Architecture +
+
+
+
+ + Extension Architecture + +
+
+ + + + +
+
+
+ App Example +
+
+
+
+ + App Example + +
+
+ + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/packages/app-next/package.json b/packages/app-next/package.json new file mode 100644 index 0000000000..d2642c2fde --- /dev/null +++ b/packages/app-next/package.json @@ -0,0 +1,143 @@ +{ + "name": "example-app-next", + "version": "0.0.0", + "private": true, + "backstage": { + "role": "frontend" + }, + "bundled": true, + "dependencies": { + "@backstage/app-defaults": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-app-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/integration-react": "workspace:^", + "@backstage/plugin-adr": "workspace:^", + "@backstage/plugin-airbrake": "workspace:^", + "@backstage/plugin-apache-airflow": "workspace:^", + "@backstage/plugin-api-docs": "workspace:^", + "@backstage/plugin-azure-devops": "workspace:^", + "@backstage/plugin-azure-sites": "workspace:^", + "@backstage/plugin-badges": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", + "@backstage/plugin-catalog-graph": "workspace:^", + "@backstage/plugin-catalog-import": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-catalog-unprocessed-entities": "workspace:^", + "@backstage/plugin-circleci": "workspace:^", + "@backstage/plugin-cloudbuild": "workspace:^", + "@backstage/plugin-code-coverage": "workspace:^", + "@backstage/plugin-cost-insights": "workspace:^", + "@backstage/plugin-devtools": "workspace:^", + "@backstage/plugin-dynatrace": "workspace:^", + "@backstage/plugin-entity-feedback": "workspace:^", + "@backstage/plugin-explore": "workspace:^", + "@backstage/plugin-gcalendar": "workspace:^", + "@backstage/plugin-gcp-projects": "workspace:^", + "@backstage/plugin-github-actions": "workspace:^", + "@backstage/plugin-gocd": "workspace:^", + "@backstage/plugin-graphiql": "workspace:^", + "@backstage/plugin-home": "workspace:^", + "@backstage/plugin-jenkins": "workspace:^", + "@backstage/plugin-kafka": "workspace:^", + "@backstage/plugin-kubernetes": "workspace:^", + "@backstage/plugin-lighthouse": "workspace:^", + "@backstage/plugin-linguist": "workspace:^", + "@backstage/plugin-linguist-common": "workspace:^", + "@backstage/plugin-microsoft-calendar": "workspace:^", + "@backstage/plugin-newrelic": "workspace:^", + "@backstage/plugin-newrelic-dashboard": "workspace:^", + "@backstage/plugin-octopus-deploy": "workspace:^", + "@backstage/plugin-org": "workspace:^", + "@backstage/plugin-pagerduty": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", + "@backstage/plugin-playlist": "workspace:^", + "@backstage/plugin-puppetdb": "workspace:^", + "@backstage/plugin-rollbar": "workspace:^", + "@backstage/plugin-scaffolder": "workspace:^", + "@backstage/plugin-scaffolder-react": "workspace:^", + "@backstage/plugin-search": "workspace:^", + "@backstage/plugin-search-common": "workspace:^", + "@backstage/plugin-search-react": "workspace:^", + "@backstage/plugin-sentry": "workspace:^", + "@backstage/plugin-shortcuts": "workspace:^", + "@backstage/plugin-stack-overflow": "workspace:^", + "@backstage/plugin-stackstorm": "workspace:^", + "@backstage/plugin-tech-insights": "workspace:^", + "@backstage/plugin-tech-radar": "workspace:^", + "@backstage/plugin-techdocs": "workspace:^", + "@backstage/plugin-techdocs-module-addons-contrib": "workspace:^", + "@backstage/plugin-techdocs-react": "workspace:^", + "@backstage/plugin-todo": "workspace:^", + "@backstage/plugin-user-settings": "workspace:^", + "@backstage/theme": "workspace:^", + "@internal/plugin-catalog-customized": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.61", + "@octokit/rest": "^19.0.3", + "@oriflame/backstage-plugin-score-card": "^0.7.0", + "@roadiehq/backstage-plugin-buildkite": "^2.0.8", + "@roadiehq/backstage-plugin-github-insights": "^2.0.5", + "@roadiehq/backstage-plugin-github-pull-requests": "^2.2.7", + "@roadiehq/backstage-plugin-travis-ci": "^2.0.5", + "history": "^5.0.0", + "lodash": "^4.17.21", + "prop-types": "^15.7.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-router": "^6.3.0", + "react-router-dom": "^6.3.0", + "react-use": "^17.2.4", + "zen-observable": "^0.10.0" + }, + "devDependencies": { + "@backstage/test-utils": "workspace:^", + "@testing-library/cypress": "^9.0.0", + "@testing-library/dom": "^8.0.0", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/jquery": "^3.3.34", + "@types/node": "^16.11.26", + "@types/react": "*", + "@types/react-dom": "*", + "@types/zen-observable": "^0.8.0", + "cross-env": "^7.0.0", + "cypress": "^10.0.0", + "eslint-plugin-cypress": "^2.10.3", + "start-server-and-test": "^1.10.11" + }, + "scripts": { + "start": "backstage-cli package start --config ../../app-config.yaml --config app-config.yaml", + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "test": "backstage-cli package test", + "lint": "backstage-cli package lint", + "test:e2e": "start-server-and-test start http://localhost:3000 cy:dev", + "test:e2e:ci": "start-server-and-test start http://localhost:3000 cy:run", + "cy:dev": "cypress open", + "cy:run": "cypress run" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "files": [ + "dist" + ], + "license": "Apache-2.0" +} diff --git a/packages/app-next/public/android-chrome-192x192.png b/packages/app-next/public/android-chrome-192x192.png new file mode 100644 index 0000000000..eec0ae25b9 Binary files /dev/null and b/packages/app-next/public/android-chrome-192x192.png differ diff --git a/packages/app-next/public/android-chrome-512x512.png b/packages/app-next/public/android-chrome-512x512.png new file mode 100644 index 0000000000..ac0578133b Binary files /dev/null and b/packages/app-next/public/android-chrome-512x512.png differ diff --git a/packages/app-next/public/apple-touch-icon.png b/packages/app-next/public/apple-touch-icon.png new file mode 100644 index 0000000000..6a45afb38f Binary files /dev/null and b/packages/app-next/public/apple-touch-icon.png differ diff --git a/packages/app-next/public/favicon-16x16.png b/packages/app-next/public/favicon-16x16.png new file mode 100644 index 0000000000..58cf61a35e Binary files /dev/null and b/packages/app-next/public/favicon-16x16.png differ diff --git a/packages/app-next/public/favicon-32x32.png b/packages/app-next/public/favicon-32x32.png new file mode 100644 index 0000000000..c0915ece75 Binary files /dev/null and b/packages/app-next/public/favicon-32x32.png differ diff --git a/packages/app-next/public/favicon.ico b/packages/app-next/public/favicon.ico new file mode 100644 index 0000000000..5e45e5dfbd Binary files /dev/null and b/packages/app-next/public/favicon.ico differ diff --git a/packages/app-next/public/favicon.svg b/packages/app-next/public/favicon.svg new file mode 100644 index 0000000000..7f2d375cd8 --- /dev/null +++ b/packages/app-next/public/favicon.svg @@ -0,0 +1 @@ +Backstage favicon \ No newline at end of file diff --git a/packages/app-next/public/index.html b/packages/app-next/public/index.html new file mode 100644 index 0000000000..0710a0d69d --- /dev/null +++ b/packages/app-next/public/index.html @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + <%= config.getString('app.title') %> + + <% if (config.has('app.datadogRum')) { %> + + <% } %> + + + + +
+ + + diff --git a/packages/app-next/public/manifest.json b/packages/app-next/public/manifest.json new file mode 100644 index 0000000000..4a7c1b4ec4 --- /dev/null +++ b/packages/app-next/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Backstage", + "name": "Backstage", + "icons": [ + { + "src": "favicon.ico", + "sizes": "48x48", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/packages/app-next/public/robots.txt b/packages/app-next/public/robots.txt new file mode 100644 index 0000000000..01b0f9a107 --- /dev/null +++ b/packages/app-next/public/robots.txt @@ -0,0 +1,2 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * diff --git a/packages/app-next/public/safari-pinned-tab.svg b/packages/app-next/public/safari-pinned-tab.svg new file mode 100644 index 0000000000..0f500b3002 --- /dev/null +++ b/packages/app-next/public/safari-pinned-tab.svg @@ -0,0 +1 @@ +Created by potrace 1.11, written by Peter Selinger 2001-2013 \ No newline at end of file diff --git a/packages/app-next/src/App.test.tsx b/packages/app-next/src/App.test.tsx new file mode 100644 index 0000000000..047c9821b5 --- /dev/null +++ b/packages/app-next/src/App.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderWithEffects } from '@backstage/test-utils'; + +describe('App', () => { + it('should render', async () => { + process.env = { + NODE_ENV: 'test', + APP_CONFIG: [ + { + data: { + app: { + title: 'Test', + support: { url: 'http://localhost:7007/support' }, + }, + backend: { baseUrl: 'http://localhost:7007' }, + lighthouse: { + baseUrl: 'http://localhost:3003', + }, + techdocs: { + storageUrl: 'http://localhost:7007/api/techdocs/static/docs', + }, + }, + context: 'test', + }, + ] as any, + }; + + const { default: App } = await import('./App'); + const rendered = await renderWithEffects(); + expect(rendered.baseElement).toBeInTheDocument(); + }); +}); diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx new file mode 100644 index 0000000000..b875fb3927 --- /dev/null +++ b/packages/app-next/src/App.tsx @@ -0,0 +1,113 @@ +/* + * 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 { graphiqlPlugin as legacyGraphiqlPlugin } from '@backstage/plugin-graphiql'; +import { createApp as createLegacyApp } from '@backstage/app-defaults'; +import { createApp } from '@backstage/frontend-app-api'; +import { graphiqlPlugin } from './examples/graphiqlPlugin'; + +/* + +# Notes + +TODO: + - proper createApp + - connect extensions and plugins, provide method? + - higher level API for creating standard extensions + higher order framework API for creating those? + - extension config schema + validation + - figure out how to resolve configured extension ref to runtime value, e.g. '@backstage/plugin-graphiql#GraphiqlPage' + - make sure all shorthands work + tests + - figure out package structure / how to ship, frontend-plugin-api/frontend-app-api + - figure out routing, useRouteRef in the new system + - Legacy plugins / interop + - dynamic updates, runtime API + +*/ + +/* core */ + +// const discoverPackages = async () => { +// // stub for now, deferring package discovery til later +// return ['@backstage/plugin-graphiql']; +// }; + +/* graphiql package */ + +/* app.tsx */ + +const app = createApp({ + plugins: [graphiqlPlugin], + // bindRoutes({ bind }) { + // bind(catalogPlugin.externalRoutes, { + // createComponent: scaffolderPlugin.routes.root, + // }); + // bind(scaffolderPlugin.externalRoutes, { + // registerComponent: catalogImportPlugin.routes.importPage, + // }); + // }, +}); + +const legacyApp = createLegacyApp({ plugins: [legacyGraphiqlPlugin] }); + +export default legacyApp.createRoot(app.createRoot()); + +// const routes = ( +// +// {/* } /> +// } /> +// } +// > +// +// +// +// +// +// + +// +// +// +// +// + +// +// +// +// +// +// +// } +// /> */} +// {/* } +// /> */} +// } /> +// +// ); + +// export default app.createRoot( +// <> +// {/* +// */} +// {routes} +// , +// ); diff --git a/packages/app-next/src/examples/graphiqlPlugin.tsx b/packages/app-next/src/examples/graphiqlPlugin.tsx new file mode 100644 index 0000000000..87f2bb242c --- /dev/null +++ b/packages/app-next/src/examples/graphiqlPlugin.tsx @@ -0,0 +1,49 @@ +/* + * 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 React from 'react'; +import { + createExtension, + createPlugin, + coreExtensionData, +} from '@backstage/frontend-plugin-api'; +import { Router as GraphiQLPage } from '@backstage/plugin-graphiql'; + +export const GraphiqlPageExtension = createExtension({ + output: { + component: coreExtensionData.reactComponent, + path: coreExtensionData.routePath, + }, + factory({ bind, config }) { + bind.component(() => { + return ; + }); + // TODO: In need of schemas and type safety + bind.path((config as { path: string }).path); + }, +}); + +export const graphiqlPlugin = createPlugin({ + id: 'graphiql', + defaultExtensionInstances: [ + { + id: 'graphiql.page', + at: 'core.router/routes', + extension: GraphiqlPageExtension, + config: { path: '/graphiql' }, + }, + ], +}); diff --git a/packages/app-next/src/index.tsx b/packages/app-next/src/index.tsx new file mode 100644 index 0000000000..b15bc4c102 --- /dev/null +++ b/packages/app-next/src/index.tsx @@ -0,0 +1,22 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@backstage/cli/asset-types'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/packages/app-next/src/setupTests.ts b/packages/app-next/src/setupTests.ts new file mode 100644 index 0000000000..23fcbe9676 --- /dev/null +++ b/packages/app-next/src/setupTests.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 7ec7df3ecf..1ec4cf84ab 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,80 @@ # example-app +## 0.2.87-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.0 + - @backstage/integration-react@1.1.18-next.0 + - @backstage/plugin-microsoft-calendar@0.1.7-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.16-next.0 + - @backstage/plugin-apache-airflow@0.2.15-next.0 + - @backstage/plugin-github-actions@0.6.4-next.0 + - @backstage/plugin-octopus-deploy@0.2.5-next.0 + - @backstage/plugin-azure-devops@0.3.5-next.0 + - @backstage/plugin-gcp-projects@0.3.41-next.0 + - @backstage/plugin-azure-sites@0.1.12-next.0 + - @backstage/plugin-cloudbuild@0.3.23-next.0 + - @backstage/plugin-lighthouse@0.4.8-next.0 + - @backstage/plugin-tech-radar@0.6.8-next.0 + - @backstage/plugin-dynatrace@7.0.3-next.0 + - @backstage/plugin-gcalendar@0.3.18-next.0 + - @backstage/plugin-shortcuts@0.3.14-next.0 + - @backstage/plugin-airbrake@0.3.23-next.0 + - @backstage/plugin-circleci@0.3.23-next.0 + - @backstage/plugin-graphiql@0.2.54-next.0 + - @backstage/plugin-linguist@0.1.8-next.0 + - @backstage/plugin-newrelic@0.3.40-next.0 + - @backstage/plugin-playlist@0.1.15-next.0 + - @backstage/plugin-puppetdb@0.1.6-next.0 + - @backstage/plugin-jenkins@0.8.5-next.0 + - @backstage/plugin-rollbar@0.4.23-next.0 + - @backstage/plugin-badges@0.2.47-next.0 + - @backstage/plugin-sentry@0.5.8-next.0 + - @backstage/plugin-kafka@0.3.23-next.0 + - @backstage/plugin-nomad@0.1.4-next.0 + - @backstage/plugin-todo@0.2.25-next.0 + - @backstage/plugin-org@0.6.13-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/plugin-techdocs@1.6.8-next.0 + - @backstage/plugin-entity-feedback@0.2.6-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/plugin-code-coverage@0.2.16-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/plugin-cost-insights@0.12.12-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/plugin-adr@0.6.6-next.0 + - @backstage/plugin-catalog-import@0.9.13-next.0 + - @backstage/plugin-scaffolder@1.14.4-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.18-next.0 + - @backstage/plugin-home@0.5.7-next.0 + - @backstage/plugin-pagerduty@0.6.4-next.0 + - @backstage/plugin-stack-overflow@0.1.20-next.0 + - @backstage/plugin-api-docs@0.9.10-next.0 + - @internal/plugin-catalog-customized@0.0.14-next.0 + - @backstage/plugin-catalog-graph@0.2.35-next.0 + - @backstage/plugin-explore@0.4.9-next.0 + - @backstage/plugin-scaffolder-react@1.5.4-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-devtools@0.1.4-next.0 + - @backstage/plugin-gocd@0.1.29-next.0 + - @backstage/plugin-kubernetes@0.10.2-next.0 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-search@1.3.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + - @backstage/plugin-stackstorm@0.1.6-next.0 + - @backstage/plugin-tech-insights@0.3.15-next.0 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + - @backstage/plugin-user-settings@0.7.8-next.0 + ## 0.2.86 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 514f95c252..cd384fdd57 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.86", + "version": "0.2.87-next.0", "private": true, "backstage": { "role": "frontend" diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index 1f1480b705..9a249b4305 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/backend-app-api +## 0.5.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-node@0.7.13-next.0 + ## 0.5.0 ### Minor Changes diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 82e32311aa..a21fea6f00 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-app-api", "description": "Core API used by Backstage backend apps", - "version": "0.5.0", + "version": "0.5.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 030223606f..c98538b8dd 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/backend-common +## 0.19.4-next.0 + +### Patch Changes + +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-dev-utils@0.1.1 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/types@1.1.0 + ## 0.19.2 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index f8e535c8a7..f0fa15efb0 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -9,6 +9,7 @@ import { AppConfig } from '@backstage/config'; import { AwsCredentialsManager } from '@backstage/integration-aws-node'; import { AwsS3Integration } from '@backstage/integration'; +import { AzureDevOpsCredentialsProvider } from '@backstage/integration'; import { AzureIntegration } from '@backstage/integration'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketCloudIntegration } from '@backstage/integration'; @@ -94,6 +95,7 @@ export class AzureUrlReader implements UrlReader { integration: AzureIntegration, deps: { treeResponseFactory: ReadTreeResponseFactory; + credentialsProvider: AzureDevOpsCredentialsProvider; }, ); // (undocumented) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index fd9b9cb0df..024c5f7e4f 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.19.2", + "version": "0.19.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -92,6 +92,7 @@ "minimatch": "^5.0.0", "minimist": "^1.2.5", "morgan": "^1.10.0", + "mysql2": "^2.2.5", "node-fetch": "^2.6.7", "node-forge": "^1.3.1", "pg": "^8.3.0", diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index 69386d77c5..bf4af17d05 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -425,6 +425,33 @@ describe('DatabaseManager', () => { ); }); + it('generates a database name override when prefix is not explicitly set for mysql', async () => { + const testManager = DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'mysql', + connection: { + host: 'localhost', + user: 'foo', + password: 'bar', + database: 'foodb', + }, + }, + }, + }), + ); + + await testManager.forPlugin('testplugin').getClient(); + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [_baseConfig, overrides] = mockCalls[0]; + + expect(overrides).toHaveProperty( + 'connection.database', + expect.stringContaining('backstage_plugin_'), + ); + }); + it('uses values from plugin connection string if top level client should be used', async () => { const pluginId = 'stringoverride'; await manager.forPlugin(pluginId).getClient(); diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index f969b3b7eb..6b2f41a2bf 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -17,7 +17,11 @@ import { ConfigReader } from '@backstage/config'; import { AzureIntegration, + DefaultAzureDevOpsCredentialsProvider, readAzureIntegrationConfig, + ScmIntegrations, + AzureDevOpsCredentialLike, + AzureIntegrationConfig, } from '@backstage/integration'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -31,12 +35,40 @@ import { getVoidLogger } from '../logging'; import { AzureUrlReader } from './AzureUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; +type AzureIntegrationConfigLike = Partial< + Omit +> & { + credentials?: Partial[]; +}; + const logger = getVoidLogger(); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }); +const urlReaderFactory = (azureIntegration: AzureIntegrationConfigLike) => { + const credentialsProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations( + ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [azureIntegration], + }, + }), + ), + ); + return new AzureUrlReader( + new AzureIntegration( + readAzureIntegrationConfig(new ConfigReader(azureIntegration)), + ), + { + treeResponseFactory, + credentialsProvider, + }, + ); +}; + const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; describe('AzureUrlReader', () => { @@ -68,13 +100,29 @@ describe('AzureUrlReader', () => { ); }); - const createConfig = (token?: string) => - new ConfigReader( + const createConfig = (token?: string) => { + let credentials: AzureDevOpsCredentialLike[] | undefined = undefined; + if (token !== undefined) { + credentials = [ + { + personalAccessToken: token, + }, + ]; + } + return new ConfigReader( { - integrations: { azure: [{ host: 'dev.azure.com', token }] }, + integrations: { + azure: [ + { + host: 'dev.azure.com', + credentials: credentials, + }, + ], + }, }, 'test-config', ); + }; it.each([ { @@ -104,8 +152,8 @@ describe('AzureUrlReader', () => { url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml', config: createConfig(undefined), response: expect.objectContaining({ - headers: expect.not.objectContaining({ - authorization: expect.anything(), + headers: expect.objectContaining({ + authorization: expect.stringMatching(/^Bearer /), }), }), }, @@ -137,7 +185,7 @@ describe('AzureUrlReader', () => { url: '', config: createConfig(''), error: - "Invalid type in config for key 'integrations.azure[0].token' in 'test-config', got empty-string, wanted string", + "Invalid type in config for key 'integrations.azure[0].credentials[0].personalAccessToken' in 'test-config', got empty-string, wanted string", }, ])('should handle error path %#', async ({ url, config, error }) => { await expect(async () => { @@ -156,16 +204,14 @@ describe('AzureUrlReader', () => { path.resolve(__dirname, '__fixtures__/mock-main.zip'), ); - const processor = new AzureUrlReader( - new AzureIntegration( - readAzureIntegrationConfig( - new ConfigReader({ - host: 'dev.azure.com', - }), - ), - ), - { treeResponseFactory }, - ); + const processor = urlReaderFactory({ + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'my-pat', + }, + ], + }); beforeEach(() => { worker.use( @@ -268,16 +314,14 @@ describe('AzureUrlReader', () => { path.resolve(__dirname, '__fixtures__/mock-main.zip'), ); - const processor = new AzureUrlReader( - new AzureIntegration( - readAzureIntegrationConfig( - new ConfigReader({ - host: 'dev.azure.com', - }), - ), - ), - { treeResponseFactory }, - ); + const processor = urlReaderFactory({ + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'my-pat', + }, + ], + }); beforeEach(() => { worker.use( diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index 94abf3a178..783736b639 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -15,12 +15,13 @@ */ import { - AzureIntegration, getAzureCommitsUrl, getAzureDownloadUrl, getAzureFileFetchUrl, - getAzureRequestOptions, + AzureDevOpsCredentialsProvider, + DefaultAzureDevOpsCredentialsProvider, ScmIntegrations, + AzureIntegration, } from '@backstage/integration'; import fetch, { Response } from 'node-fetch'; import { Minimatch } from 'minimatch'; @@ -47,8 +48,13 @@ import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; export class AzureUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const integrations = ScmIntegrations.fromConfig(config); + const credentialProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(integrations); return integrations.azure.list().map(integration => { - const reader = new AzureUrlReader(integration, { treeResponseFactory }); + const reader = new AzureUrlReader(integration, { + treeResponseFactory, + credentialsProvider: credentialProvider, + }); const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); @@ -56,7 +62,10 @@ export class AzureUrlReader implements UrlReader { constructor( private readonly integration: AzureIntegration, - private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, + private readonly deps: { + treeResponseFactory: ReadTreeResponseFactory; + credentialsProvider: AzureDevOpsCredentialsProvider; + }, ) {} async read(url: string): Promise { @@ -72,11 +81,13 @@ export class AzureUrlReader implements UrlReader { const { signal } = options ?? {}; const builtUrl = getAzureFileFetchUrl(url); - let response: Response; try { + const credentials = await this.deps.credentialsProvider.getCredentials({ + url: builtUrl, + }); response = await fetch(builtUrl, { - ...(await getAzureRequestOptions(this.integration.config)), + headers: credentials?.headers, // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can @@ -111,10 +122,13 @@ export class AzureUrlReader implements UrlReader { // Get latest commit SHA - const commitsAzureResponse = await fetch( - getAzureCommitsUrl(url), - await getAzureRequestOptions(this.integration.config), - ); + const credentials = await this.deps.credentialsProvider.getCredentials({ + url: url, + }); + + const commitsAzureResponse = await fetch(getAzureCommitsUrl(url), { + headers: credentials?.headers, + }); if (!commitsAzureResponse.ok) { const message = `Failed to read tree from ${url}, ${commitsAzureResponse.status} ${commitsAzureResponse.statusText}`; if (commitsAzureResponse.status === 404) { @@ -129,9 +143,10 @@ export class AzureUrlReader implements UrlReader { } const archiveAzureResponse = await fetch(getAzureDownloadUrl(url), { - ...(await getAzureRequestOptions(this.integration.config, { + headers: { + ...credentials?.headers, Accept: 'application/zip', - })), + }, // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can be @@ -198,7 +213,9 @@ export class AzureUrlReader implements UrlReader { } toString() { - const { host, token } = this.integration.config; - return `azure{host=${host},authed=${Boolean(token)}}`; + const { host, credentials } = this.integration.config; + return `azure{host=${host},authed=${Boolean( + credentials !== undefined && credentials.length > 0, + )}}`; } } diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index af86313d11..d027d760f1 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-defaults +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + ## 0.2.0 ### Minor Changes diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 48464ac81e..3cf1a260cc 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-defaults", "description": "Backend defaults used by Backstage backend apps", - "version": "0.2.0", + "version": "0.2.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-next/CHANGELOG.md b/packages/backend-next/CHANGELOG.md index 2c0fefb9f3..ccc4b83076 100644 --- a/packages/backend-next/CHANGELOG.md +++ b/packages/backend-next/CHANGELOG.md @@ -1,5 +1,38 @@ # example-backend-next +## 0.0.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-scaffolder-backend@1.16.3-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/plugin-linguist-backend@0.4.2-next.0 + - @backstage/plugin-entity-feedback-backend@0.1.8-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/plugin-app-backend@0.3.50-next.0 + - @backstage/backend-defaults@0.2.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-adr-backend@0.3.8-next.0 + - @backstage/plugin-azure-devops-backend@0.3.29-next.0 + - @backstage/plugin-badges-backend@0.2.5-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.2-next.0 + - @backstage/plugin-devtools-backend@0.1.5-next.0 + - @backstage/plugin-kubernetes-backend@0.11.5-next.0 + - @backstage/plugin-lighthouse-backend@0.2.6-next.0 + - @backstage/plugin-permission-backend@0.5.25-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-proxy-backend@0.3.2-next.0 + - @backstage/plugin-search-backend@1.4.2-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.6-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.6-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.6-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-techdocs-backend@1.6.7-next.0 + - @backstage/plugin-todo-backend@0.2.2-next.0 + ## 0.0.14 ### Patch Changes diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index c2b923ed26..39bb23de15 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-next", - "version": "0.0.14", + "version": "0.0.15-next.0", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index 9ea8b95c03..7584c9a37c 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/backend-plugin-api +## 0.6.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + ## 0.6.0 ### Minor Changes diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 44a9d5c69c..83b8fc09dc 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-plugin-api", "description": "Core API used by Backstage backend plugins", - "version": "0.6.0", + "version": "0.6.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 11a039e053..80e07f540f 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/backend-tasks +## 0.5.7-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + ## 0.5.5 ### Patch Changes diff --git a/packages/backend-tasks/migrations/20210928160613_init.js b/packages/backend-tasks/migrations/20210928160613_init.js index 21d80ae24c..f9900cab11 100644 --- a/packages/backend-tasks/migrations/20210928160613_init.js +++ b/packages/backend-tasks/migrations/20210928160613_init.js @@ -26,7 +26,7 @@ exports.up = async function up(knex) { await knex.schema.createTable('backstage_backend_tasks__tasks', table => { table.comment('Tasks used for scheduling work on multiple workers'); table - .text('id') + .string('id') .primary() .notNullable() .comment('The unique ID of this particular task'); diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index ca9129078e..50e739ce8d 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.5.5", + "version": "0.5.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-tasks/src/migrations.test.ts b/packages/backend-tasks/src/migrations.test.ts index 7e471a1c16..1d701c971d 100644 --- a/packages/backend-tasks/src/migrations.test.ts +++ b/packages/backend-tasks/src/migrations.test.ts @@ -43,7 +43,7 @@ jest.setTimeout(60_000); describe('migrations', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'MYSQL_8', 'SQLITE_3'], }); it.each(databases.eachSupportedId())( diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts index 5b5d592b7b..efbad2e8b6 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts @@ -42,6 +42,7 @@ describe('PluginTaskSchedulerJanitor', () => { 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', + 'MYSQL_8', ], }); diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts index 89eff433db..6aa77dd7fe 100644 --- a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts @@ -25,7 +25,7 @@ jest.setTimeout(60_000); describe('TaskScheduler', () => { const logger = getVoidLogger(); const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'], }); async function createDatabase( diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index fa2577ce53..b8b24e045a 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -28,7 +28,7 @@ jest.setTimeout(60_000); describe('TaskWorker', () => { const logger = getVoidLogger(); const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'], }); beforeEach(() => { diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 483fb1a02d..a03d1ec058 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -175,11 +175,15 @@ export class TaskWorker { const time = new CronTime(settings.cadence) .sendAt() .minus({ seconds: 1 }) // immediately, if "* * * * * *" - .toUTC() - .toISO(); - startAt = this.knex.client.config.client.includes('sqlite3') - ? this.knex.raw('datetime(?)', [time]) - : this.knex.raw(`?`, [time]); + .toUTC(); + + if (this.knex.client.config.client.includes('sqlite3')) { + startAt = this.knex.raw('datetime(?)', [time.toISO()]); + } else if (this.knex.client.config.client.includes('mysql')) { + startAt = this.knex.raw(`?`, [time.toSQL({ includeOffset: false })]); + } else { + startAt = this.knex.raw(`?`, [time.toISO()]); + } } else { startAt = this.knex.fn.now(); } @@ -279,11 +283,16 @@ export class TaskWorker { let nextRun: Knex.Raw; if (isCron) { - const time = new CronTime(settings.cadence).sendAt().toUTC().toISO(); + const time = new CronTime(settings.cadence).sendAt().toUTC(); this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); - nextRun = this.knex.client.config.client.includes('sqlite3') - ? this.knex.raw('datetime(?)', [time]) - : this.knex.raw(`?`, [time]); + + if (this.knex.client.config.client.includes('sqlite3')) { + nextRun = this.knex.raw('datetime(?)', [time.toISO()]); + } else if (this.knex.client.config.client.includes('mysql')) { + nextRun = this.knex.raw(`?`, [time.toSQL({ includeOffset: false })]); + } else { + nextRun = this.knex.raw(`?`, [time.toISO()]); + } } else { const dt = Duration.fromISO(settings.cadence).as('seconds'); this.logger.debug( diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index d6eec9f7ed..006d2b8214 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/backend-test-utils +## 0.2.2-next.0 + +### Patch Changes + +- 9fb3b5373c45: Extended `mockService` to also include mocked variants, for example `mockServices.lifecycle.mock()`. The returned mocked implementation will have a `factory` property which is a service factory for itself. You can also pass a partial implementation of the service to the mock function to use a mock implementation of specific methods. +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + ## 0.2.0 ### Minor Changes diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index d086b1d2ae..95f67da35f 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { CacheService } from '@backstage/backend-plugin-api'; @@ -19,6 +21,7 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; +import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; @@ -33,11 +36,19 @@ export namespace mockServices { export namespace cache { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace database { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace httpRouter { @@ -45,6 +56,10 @@ export namespace mockServices { factory: ( options?: HttpRouterFactoryOptions | undefined, ) => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function identity(): IdentityService; @@ -52,21 +67,37 @@ export namespace mockServices { export namespace identity { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace lifecycle { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace logger { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace permissions { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function rootConfig(options?: rootConfig.Options): RootConfigService; @@ -85,6 +116,10 @@ export namespace mockServices { export namespace rootLifecycle { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function rootLogger(options?: rootLogger.Options): LoggerService; @@ -98,11 +133,19 @@ export namespace mockServices { factory: ( options?: Options | undefined, ) => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace scheduler { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function tokenManager(): TokenManagerService; @@ -110,14 +153,34 @@ export namespace mockServices { export namespace tokenManager { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace urlReader { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } } +// @public (undocumented) +export type ServiceMock = { + factory: ServiceFactory; +} & { + [Key in keyof TService]: TService[Key] extends ( + this: infer This, + ...args: infer Args + ) => infer Return + ? TService[Key] & jest.MockInstance + : TService[Key]; +}; + // @public export function setupRequestMockHandlers(worker: { listen: (t: any) => void; diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 02cb1d5cf9..ef0ccfdd9d 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.2.0", + "version": "0.2.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -57,6 +57,9 @@ "testcontainers": "^8.1.2", "uuid": "^8.0.0" }, + "peerDependencies": { + "@types/jest": "*" + }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", diff --git a/packages/backend-test-utils/src/next/services/index.ts b/packages/backend-test-utils/src/next/services/index.ts index f562150efa..97a22567fd 100644 --- a/packages/backend-test-utils/src/next/services/index.ts +++ b/packages/backend-test-utils/src/next/services/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { mockServices } from './mockServices'; +export { mockServices, type ServiceMock } from './mockServices'; diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 0455b9f1ae..3edc3e0894 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -40,6 +40,7 @@ import { JsonObject } from '@backstage/types'; import { MockIdentityService } from './MockIdentityService'; import { MockRootLoggerService } from './MockRootLoggerService'; +/** @internal */ function simpleFactory< TService, TScope extends 'root' | 'plugin', @@ -57,6 +58,44 @@ function simpleFactory< })) as (...options: TOptions) => ServiceFactory; } +/** @public */ +export type ServiceMock = { + factory: ServiceFactory; +} & { + [Key in keyof TService]: TService[Key] extends ( + this: infer This, + ...args: infer Args + ) => infer Return + ? TService[Key] & jest.MockInstance + : TService[Key]; +}; + +/** @internal */ +function simpleMock( + ref: ServiceRef, + mockFactory: () => jest.Mocked, +): (partialImpl?: Partial) => ServiceMock { + return partialImpl => { + const mock = mockFactory(); + if (partialImpl) { + for (const [key, impl] of Object.entries(partialImpl)) { + if (typeof impl === 'function') { + (mock as any)[key].mockImplementation(impl); + } else { + (mock as any)[key] = impl; + } + } + } + return Object.assign(mock, { + factory: createServiceFactory({ + service: ref, + deps: {}, + factory: () => mock, + })(), + }) as ServiceMock; + }; +} + /** * @public */ @@ -79,6 +118,13 @@ export namespace mockServices { }; export const factory = simpleFactory(coreServices.rootLogger, rootLogger); + export const mock = simpleMock(coreServices.rootLogger, () => ({ + child: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + })); } export function tokenManager(): TokenManagerService { @@ -98,6 +144,10 @@ export namespace mockServices { coreServices.tokenManager, tokenManager, ); + export const mock = simpleMock(coreServices.tokenManager, () => ({ + authenticate: jest.fn(), + getToken: jest.fn(), + })); } export function identity(): IdentityService { @@ -105,6 +155,9 @@ export namespace mockServices { } export namespace identity { export const factory = simpleFactory(coreServices.identity, identity); + export const mock = simpleMock(coreServices.identity, () => ({ + getIdentity: jest.fn(), + })); } // TODO(Rugvip): Not all core services have implementations available here yet. @@ -112,29 +165,71 @@ export namespace mockServices { // re-implement functioning mock versions here. export namespace cache { export const factory = cacheServiceFactory; + export const mock = simpleMock(coreServices.cache, () => ({ + delete: jest.fn(), + get: jest.fn(), + set: jest.fn(), + 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, () => ({ + use: jest.fn(), + })); } export namespace lifecycle { export const factory = lifecycleServiceFactory; + export const mock = simpleMock(coreServices.lifecycle, () => ({ + addShutdownHook: jest.fn(), + addStartupHook: jest.fn(), + })); } export namespace logger { export const factory = loggerServiceFactory; + export const mock = simpleMock(coreServices.logger, () => ({ + child: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + })); } export namespace permissions { export const factory = permissionsServiceFactory; + export const mock = simpleMock(coreServices.permissions, () => ({ + authorize: jest.fn(), + authorizeConditional: jest.fn(), + })); } export namespace rootLifecycle { export const factory = rootLifecycleServiceFactory; + export const mock = simpleMock(coreServices.rootLifecycle, () => ({ + addShutdownHook: jest.fn(), + addStartupHook: jest.fn(), + })); } export namespace scheduler { export const factory = schedulerServiceFactory; + export const mock = simpleMock(coreServices.scheduler, () => ({ + createScheduledTaskRunner: jest.fn(), + getScheduledTasks: jest.fn(), + scheduleTask: jest.fn(), + triggerTask: jest.fn(), + })); } export namespace urlReader { export const factory = urlReaderServiceFactory; + export const mock = simpleMock(coreServices.urlReader, () => ({ + readTree: jest.fn(), + readUrl: jest.fn(), + search: jest.fn(), + })); } } diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 76ac221f69..b54bc46ebf 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,60 @@ # example-backend +## 0.2.87-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-auth-backend@0.18.8-next.0 + - @backstage/plugin-code-coverage-backend@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend@1.16.3-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/plugin-linguist-backend@0.4.2-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/plugin-entity-feedback-backend@0.1.8-next.0 + - @backstage/plugin-tech-insights-backend@0.5.16-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/plugin-app-backend@0.3.50-next.0 + - example-app@0.2.87-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-adr-backend@0.3.8-next.0 + - @backstage/plugin-azure-devops-backend@0.3.29-next.0 + - @backstage/plugin-azure-sites-backend@0.1.12-next.0 + - @backstage/plugin-badges-backend@0.2.5-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.2-next.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-devtools-backend@0.1.5-next.0 + - @backstage/plugin-events-backend@0.2.11-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-explore-backend@0.0.12-next.0 + - @backstage/plugin-graphql-backend@0.1.40-next.0 + - @backstage/plugin-jenkins-backend@0.2.5-next.0 + - @backstage/plugin-kafka-backend@0.2.43-next.0 + - @backstage/plugin-kubernetes-backend@0.11.5-next.0 + - @backstage/plugin-lighthouse-backend@0.2.6-next.0 + - @backstage/plugin-nomad-backend@0.1.4-next.0 + - @backstage/plugin-permission-backend@0.5.25-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-playlist-backend@0.3.6-next.0 + - @backstage/plugin-proxy-backend@0.3.2-next.0 + - @backstage/plugin-rollbar-backend@0.1.47-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.3-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.19-next.0 + - @backstage/plugin-search-backend@1.4.2-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.5-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.11-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.34-next.0 + - @backstage/plugin-tech-insights-node@0.4.8-next.0 + - @backstage/plugin-techdocs-backend@1.6.7-next.0 + - @backstage/plugin-todo-backend@0.2.2-next.0 + ## 0.2.86 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 9e0896233d..6d50cd0f28 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.86", + "version": "0.2.87-next.0", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -87,6 +87,7 @@ "express-prom-bundle": "^6.3.6", "express-promise-router": "^4.1.0", "luxon": "^3.0.0", + "mysql2": "^2.2.5", "pg": "^8.3.0", "pg-connection-string": "^2.3.0", "prom-client": "^14.0.1", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 9294e55efd..b41a75a6ca 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/cli +## 0.22.12-next.0 + +### Patch Changes + +- 278d9326eb40: Added the ability to create a plain backend module with the `new` command. +- 71d4368ae5cc: Added support for the `dev/index` entry point for backend plugins and modules. +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/eslint-plugin@0.1.3 + - @backstage/release-manifests@0.0.9 + - @backstage/types@1.1.0 + ## 0.22.10 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 95095fe78c..4efb27e625 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.22.10", + "version": "0.22.12-next.0", "publishConfig": { "access": "public" }, @@ -140,6 +140,8 @@ }, "devDependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", "@backstage/config": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/core-components": "workspace:^", diff --git a/packages/cli/src/commands/start/startBackend.ts b/packages/cli/src/commands/start/startBackend.ts index 3e6a13be96..622eeb9d1b 100644 --- a/packages/cli/src/commands/start/startBackend.ts +++ b/packages/cli/src/commands/start/startBackend.ts @@ -26,7 +26,17 @@ interface StartBackendOptions { } export async function startBackend(options: StartBackendOptions) { - if (process.env.EXPERIMENTAL_BACKEND_START) { + const hasDev = await fs.pathExists(paths.resolveTarget('dev')); + if (hasDev) { + const waitForExit = await startBackendExperimental({ + entry: 'dev/index', + checksEnabled: false, // not supported + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); + + await waitForExit(); + } else if (process.env.EXPERIMENTAL_BACKEND_START) { const waitForExit = await startBackendExperimental({ entry: 'src/index', checksEnabled: false, // not supported diff --git a/packages/cli/src/lib/new/factories/backendModule.test.ts b/packages/cli/src/lib/new/factories/backendModule.test.ts new file mode 100644 index 0000000000..c633b1c1b2 --- /dev/null +++ b/packages/cli/src/lib/new/factories/backendModule.test.ts @@ -0,0 +1,121 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; +import { backendModule } from './backendModule'; + +describe('backendModule factory', () => { + beforeEach(() => { + mockPaths({ + targetRoot: '/root', + }); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a backend plugin', async () => { + mockFs({ + '/root': { + packages: { + backend: { + 'package.json': JSON.stringify({}), + }, + }, + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(backendModule, { + id: 'test', + moduleId: 'tester-two', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await backendModule.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + '', + 'Creating backend module backstage-plugin-test-backend-module-tester-two', + 'Checking Prerequisites:', + `availability plugins${sep}test-backend-module-tester-two`, + 'creating temp dir', + 'Executing Template:', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'templating index.ts.hbs', + 'templating module.ts.hbs', + 'Installing:', + `moving plugins${sep}test-backend-module-tester-two`, + 'backend adding dependency', + ]); + + await expect( + fs.readJson('/root/packages/backend/package.json'), + ).resolves.toEqual({ + dependencies: { + 'backstage-plugin-test-backend-module-tester-two': '^1.0.0', + }, + }); + const moduleFile = await fs.readFile( + '/root/plugins/test-backend-module-tester-two/src/module.ts', + 'utf-8', + ); + + expect(moduleFile).toContain( + `const testModuleTesterTwo = createBackendModule({`, + ); + expect(moduleFile).toContain(`pluginId: 'test',`); + expect(moduleFile).toContain(`moduleId: 'tester-two',`); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: resolvePath('/root/plugins/test-backend-module-tester-two'), + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: resolvePath('/root/plugins/test-backend-module-tester-two'), + optional: true, + }); + }); +}); diff --git a/packages/cli/src/lib/new/factories/backendModule.ts b/packages/cli/src/lib/new/factories/backendModule.ts new file mode 100644 index 0000000000..221adcb1f3 --- /dev/null +++ b/packages/cli/src/lib/new/factories/backendModule.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import chalk from 'chalk'; +import camelCase from 'lodash/camelCase'; +import { paths } from '../../paths'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; +import { createFactory, CreateContext } from '../types'; +import { addPackageDependency, Task } from '../../tasks'; +import { + moduleIdIdPrompt, + ownerPrompt, + pluginIdPrompt, +} from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; + +type Options = { + id: string; + moduleId: string; + owner?: string; + codeOwnersPath?: string; +}; + +export const backendModule = createFactory({ + name: 'backend-module', + description: 'A new backend module', + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [pluginIdPrompt(), moduleIdIdPrompt(), ownerPrompt()], + async create(options: Options, ctx: CreateContext) { + const { id: pluginId, moduleId } = options; + const dirName = `${pluginId}-backend-module-${moduleId}`; + const name = ctx.scope + ? `@${ctx.scope}/plugin-${dirName}` + : `backstage-plugin-${dirName}`; + + Task.log(); + Task.log(`Creating backend module ${chalk.cyan(name)}`); + + const targetDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', dirName) + : paths.resolveTargetRoot(`backstage-plugin-${dirName}`); + + const moduleCamelCase = camelCase(moduleId); + const modulePascalCase = + moduleCamelCase[0].toUpperCase() + moduleCamelCase.slice(1); + const moduleVar = `${camelCase(pluginId)}Module${modulePascalCase}`; + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'default-backend-module', + values: { + pluginId, + moduleId, + name, + moduleVar, + packageVersion: ctx.defaultVersion, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + }, + }); + + if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { + await Task.forItem('backend', 'adding dependency', async () => { + await addPackageDependency( + paths.resolveTargetRoot('packages/backend/package.json'), + { + dependencies: { + [name]: `^${ctx.defaultVersion}`, + }, + }, + ); + }); + } + + if (options.owner) { + await addCodeownersEntry(`/plugins/${dirName}`, options.owner); + } + + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: targetDir, + optional: true, + }); + }, +}); diff --git a/packages/cli/src/lib/new/factories/common/prompts.ts b/packages/cli/src/lib/new/factories/common/prompts.ts index 9c7672ddfb..428ba8f9d3 100644 --- a/packages/cli/src/lib/new/factories/common/prompts.ts +++ b/packages/cli/src/lib/new/factories/common/prompts.ts @@ -33,6 +33,22 @@ export function pluginIdPrompt(): Prompt<{ id: string }> { }; } +export function moduleIdIdPrompt(): Prompt<{ moduleId: string }> { + return { + type: 'input', + name: 'moduleId', + message: 'Enter the ID of the module [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter the ID of the module'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Module IDs must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }; +} + export function ownerPrompt(): Prompt<{ owner?: string; codeOwnersPath?: string; diff --git a/packages/cli/src/lib/new/factories/index.ts b/packages/cli/src/lib/new/factories/index.ts index 232e762b91..a4bcb2b721 100644 --- a/packages/cli/src/lib/new/factories/index.ts +++ b/packages/cli/src/lib/new/factories/index.ts @@ -16,6 +16,7 @@ export { frontendPlugin } from './frontendPlugin'; export { backendPlugin } from './backendPlugin'; +export { backendModule } from './backendModule'; export { webLibraryPackage } from './webLibraryPackage'; export { pluginCommon } from './pluginCommon'; export { pluginNode } from './pluginNode'; diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index bf89842d5f..96b42fcfb7 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -35,6 +35,8 @@ leaving any imports in place. */ import { version as backendCommon } from '../../../../packages/backend-common/package.json'; +import { version as backendPluginApi } from '../../../../packages/backend-plugin-api/package.json'; +import { version as backendTestUtils } from '../../../../packages/backend-test-utils/package.json'; import { version as cli } from '../../../../packages/cli/package.json'; import { version as config } from '../../../../packages/config/package.json'; import { version as coreAppApi } from '../../../../packages/core-app-api/package.json'; @@ -47,6 +49,8 @@ import { version as scaffolderBackend } from '../../../../plugins/scaffolder-bac export const packageVersions: Record = { '@backstage/backend-common': backendCommon, + '@backstage/backend-plugin-api': backendPluginApi, + '@backstage/backend-test-utils': backendTestUtils, '@backstage/cli': cli, '@backstage/config': config, '@backstage/core-app-api': coreAppApi, diff --git a/packages/cli/templates/default-backend-module/.eslintrc.js b/packages/cli/templates/default-backend-module/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli/templates/default-backend-module/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli/templates/default-backend-module/README.md.hbs b/packages/cli/templates/default-backend-module/README.md.hbs new file mode 100644 index 0000000000..ad7669beda --- /dev/null +++ b/packages/cli/templates/default-backend-module/README.md.hbs @@ -0,0 +1,5 @@ +# {{name}} + +The {{moduleId}} backend module for the {{pluginId}} plugin. + +_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/default-backend-module/package.json.hbs b/packages/cli/templates/default-backend-module/package.json.hbs new file mode 100644 index 0000000000..07eee73d50 --- /dev/null +++ b/packages/cli/templates/default-backend-module/package.json.hbs @@ -0,0 +1,42 @@ +{ + "name": "{{name}}", + "description": "The {{moduleId}} backend module for the {{pluginId}} plugin.", + "version": "{{packageVersion}}", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", +{{#if privatePackage}} + "private": {{privatePackage}}, +{{/if}} + "publishConfig": { +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", +{{/if}} + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "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": "{{versionQuery '@backstage/backend-common'}}", + "@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}" + }, + "devDependencies": { + "@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}", + "@backstage/cli": "{{versionQuery '@backstage/cli'}}" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli/templates/default-backend-module/src/index.ts.hbs b/packages/cli/templates/default-backend-module/src/index.ts.hbs new file mode 100644 index 0000000000..0cf1f00d42 --- /dev/null +++ b/packages/cli/templates/default-backend-module/src/index.ts.hbs @@ -0,0 +1,8 @@ +/***/ +/** + * The {{moduleId}} backend module for the {{pluginId}} plugin. + * + * @packageDocumentation + */ + +export { {{moduleVar}} } from './module'; diff --git a/packages/cli/templates/default-backend-module/src/module.ts.hbs b/packages/cli/templates/default-backend-module/src/module.ts.hbs new file mode 100644 index 0000000000..8c895a49c0 --- /dev/null +++ b/packages/cli/templates/default-backend-module/src/module.ts.hbs @@ -0,0 +1,14 @@ +import { coreServices, createBackendModule } from '@backstage/backend-plugin-api'; + +export const {{moduleVar}} = createBackendModule({ + pluginId: '{{pluginId}}', + moduleId: '{{moduleId}}', + register(reg) { + reg.registerInit({ + deps: { logger: coreServices.logger }, + async init({ logger }) { + logger.info('Hello World!') + }, + }); + }, +}); diff --git a/packages/cli/templates/default-backend-module/tsconfig.json b/packages/cli/templates/default-backend-module/tsconfig.json new file mode 100644 index 0000000000..5ae9aeb62d --- /dev/null +++ b/packages/cli/templates/default-backend-module/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": "." + } +} diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 909bb71b37..5c4177f79d 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/config-loader +## 1.5.0-next.0 + +### Minor Changes + +- 9606ba0939e6: Deep visibility now also applies to values that are not covered by the configuration schema. + + For example, given the following configuration schema: + + ```ts + // plugins/a/config.schema.ts + export interface Config { + /** @deepVisibility frontend */ + a?: unknown; + } + + // plugins/a/config.schema.ts + export interface Config { + a?: { + b?: string; + }; + } + ``` + + All values under `a` are now visible to the frontend, while previously only `a` and `a/b` would've been visible. + +### Patch Changes + +- f9657b891b00: Do not unnecessarily notify subscribers when no-op updates to config happen +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + ## 1.4.0 ### Minor Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 7fba2d8acb..20612576a1 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "1.4.0", + "version": "1.5.0-next.0", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", diff --git a/packages/config-loader/src/schema/compile.test.ts b/packages/config-loader/src/schema/compile.test.ts index 914a813d9f..ff0a9543aa 100644 --- a/packages/config-loader/src/schema/compile.test.ts +++ b/packages/config-loader/src/schema/compile.test.ts @@ -39,6 +39,7 @@ describe('compileConfigSchemas', () => { }, ], visibilityByDataPath: new Map(), + deepVisibilityByDataPath: new Map(), visibilityBySchemaPath: new Map(), deprecationByDataPath: new Map(), }); @@ -53,6 +54,7 @@ describe('compileConfigSchemas', () => { }, ], visibilityByDataPath: new Map(), + deepVisibilityByDataPath: new Map(), visibilityBySchemaPath: new Map(), deprecationByDataPath: new Map(), }); @@ -106,6 +108,7 @@ describe('compileConfigSchemas', () => { '/d/0': 'frontend', }), ), + deepVisibilityByDataPath: new Map(), visibilityBySchemaPath: new Map( Object.entries({ '/properties/a': 'frontend', @@ -166,6 +169,7 @@ describe('compileConfigSchemas', () => { '/b': 'deprecation reason for b', }), ), + deepVisibilityByDataPath: new Map(), visibilityByDataPath: new Map(), visibilityBySchemaPath: new Map(), }); @@ -221,6 +225,7 @@ describe('compileConfigSchemas', () => { '//circleci/api/headers/Circle-Token': 'secret', }), ), + deepVisibilityByDataPath: new Map(), visibilityBySchemaPath: new Map( Object.entries({ '/properties//circleci/api/properties/headers/properties/Circle-Token': @@ -277,18 +282,18 @@ describe('deepVisibility', () => { ).toEqual({ visibilityByDataPath: new Map( Object.entries({ - '': 'secret', '/b': 'secret', - '/d': 'secret', - '/d/0': 'secret', + }), + ), + deepVisibilityByDataPath: new Map( + Object.entries({ + '': 'secret', }), ), visibilityBySchemaPath: new Map( Object.entries({ '': 'secret', '/properties/b': 'secret', - '/properties/d': 'secret', - '/properties/d/items': 'secret', }), ), deprecationByDataPath: new Map(), @@ -339,26 +344,6 @@ describe('deepVisibility', () => { it('should throw when children have a different deepVisibility', () => { expect(() => compileConfigSchemas([ - { - path: 'a1', - value: { - type: 'object', - properties: { - a: { - type: 'object', - properties: { - a: { type: 'string' }, - }, - }, - b: { type: 'string', visibility: 'backend' }, - c: { type: 'string' }, - d: { - type: 'array', - items: { type: 'string' }, - }, - }, - }, - }, { path: 'a2', value: { @@ -368,7 +353,7 @@ describe('deepVisibility', () => { a: { type: 'object', properties: { - a: { type: 'string', visibility: 'frontend' }, + a: { type: 'string', deepVisibility: 'frontend' }, }, }, b: { type: 'string', visibility: 'secret' }, @@ -386,6 +371,28 @@ describe('deepVisibility', () => { ); }); + it('should throw when the same schema node has a conflicting deepVisibility', () => { + expect(() => + compileConfigSchemas([ + { + path: 'a2', + value: { + type: 'object', + properties: { + a: { + type: 'string', + deepVisibility: 'secret', + visibility: 'frontend', + }, + }, + }, + }, + ]), + ).toThrow( + `Config schema visibility is both 'frontend' and 'secret' for /properties/a`, + ); + }); + it('should throw when ancestor and children have a different deepVisibility', () => { expect(() => compileConfigSchemas([ diff --git a/packages/config-loader/src/schema/compile.ts b/packages/config-loader/src/schema/compile.ts index 5d448a590a..fd39b0def9 100644 --- a/packages/config-loader/src/schema/compile.ts +++ b/packages/config-loader/src/schema/compile.ts @@ -28,6 +28,9 @@ import { import { SchemaObject } from 'json-schema-traverse'; import { normalizeAjvPath } from './utils'; +// Used to keep track of the internal deepVisibility inherited through the schema. +const inheritedVisibility = Symbol('inherited-visibility'); + /** * This takes a collection of Backstage configuration schemas from various * sources and compiles them down into a single schema validation function. @@ -45,6 +48,7 @@ export function compileConfigSchemas( // output during validation. We work around this by having this extra piece // of state that we reset before each validation. const visibilityByDataPath = new Map(); + const deepVisibilityByDataPath = new Map(); const deprecationByDataPath = new Map(); const ajv = new Ajv({ @@ -87,6 +91,18 @@ export function compileConfigSchemas( */ enum: ['frontend', 'secret'], }, + compile(visibility: 'frontend' | 'secret') { + return (_data, context) => { + if (context?.instancePath === undefined) { + return false; + } + if (visibility) { + const normalizedPath = normalizeAjvPath(context.instancePath); + deepVisibilityByDataPath.set(normalizedPath, visibility); + } + return true; + }; + }, }) .removeKeyword('deprecated') // remove `deprecated` keyword so that we can implement our own compiler .addKeyword({ @@ -118,37 +134,34 @@ export function compileConfigSchemas( traverse( merged, ( - schema: SchemaObject, + schema: SchemaObject & { [inheritedVisibility]?: ConfigVisibility }, jsonPtr: string, _1: any, _2: any, _3?: any, - parentSchema?: SchemaObject, + parentSchema?: SchemaObject & { + [inheritedVisibility]?: ConfigVisibility; + }, ) => { // Inherit parent deepVisibility if we don't define one ourselves. - if (parentSchema?.deepVisibility) { - schema.deepVisibility ??= parentSchema?.deepVisibility; - } + // This is used to detect situations where conflicting deep visibilities are set. + schema[inheritedVisibility] ??= + schema?.deepVisibility ?? parentSchema?.[inheritedVisibility]; - // Apply deep visibility to self. - if (schema?.deepVisibility) { - // This runs before we compile the AJV so visibilityByDataPath has the - // correct data. - schema.visibility ??= schema.deepVisibility; - - if (parentSchema) { - /** - * Validate that we're not trying to override a child's visibility - * by setting the parent deep visibility. - */ - const values = [schema.visibility, parentSchema.visibility]; - const hasFrontend = values.some(e => e === 'frontend'); - const hasSecret = values.some(e => e === 'secret'); - if (hasFrontend && hasSecret) { - throw new Error( - `Config schema visibility is both 'frontend' and 'secret' for ${jsonPtr}`, - ); - } + if (schema[inheritedVisibility]) { + // Validate that we're not trying to set a conflicting visibility. This can be done + // either by setting a conflicting visibility directly or deep visibility + const values = [ + schema.visibility, + schema[inheritedVisibility], + parentSchema?.[inheritedVisibility], + ]; + const hasFrontend = values.some(e => e === 'frontend'); + const hasSecret = values.some(e => e === 'secret'); + if (hasFrontend && hasSecret) { + throw new Error( + `Config schema visibility is both 'frontend' and 'secret' for ${jsonPtr}`, + ); } } @@ -171,12 +184,16 @@ export function compileConfigSchemas( if (schema.visibility && schema.visibility !== 'backend') { visibilityBySchemaPath.set(normalizeAjvPath(path), schema.visibility); } + if (schema.deepVisibility) { + visibilityBySchemaPath.set(normalizeAjvPath(path), schema.deepVisibility); + } }); return configs => { const config = ConfigReader.fromConfigs(configs).get(); visibilityByDataPath.clear(); + deepVisibilityByDataPath.clear(); const valid = validate(config); @@ -184,6 +201,7 @@ export function compileConfigSchemas( return { errors: validate.errors ?? [], visibilityByDataPath: new Map(visibilityByDataPath), + deepVisibilityByDataPath: new Map(deepVisibilityByDataPath), visibilityBySchemaPath, deprecationByDataPath, }; @@ -191,6 +209,7 @@ export function compileConfigSchemas( return { visibilityByDataPath: new Map(visibilityByDataPath), + deepVisibilityByDataPath: new Map(deepVisibilityByDataPath), visibilityBySchemaPath, deprecationByDataPath, }; diff --git a/packages/config-loader/src/schema/filtering.test.ts b/packages/config-loader/src/schema/filtering.test.ts index 3a5de7e2c0..4beed367ea 100644 --- a/packages/config-loader/src/schema/filtering.test.ts +++ b/packages/config-loader/src/schema/filtering.test.ts @@ -30,6 +30,8 @@ const data = { b: { s: true, }, + df: { a: 1, b: [{ c: 1 }, '1'], d: { e: 'x' } }, + ds: { a: 1, b: [{ c: 1 }, '1'], d: { e: 'x' } }, }, arrF: [{ never: 'here' }], arrB: [{ never: 'here' }], @@ -64,6 +66,13 @@ const visibility = new Map( }), ); +const deepVisibility = new Map( + Object.entries({ + '/obj/df': 'frontend', + '/obj/ds': 'secret', + }), +); + const deprecations = new Map( Object.entries({ '/arr': 'deprecated array', @@ -92,6 +101,14 @@ describe('filterByVisibility', () => { 'objArr[1].s', 'obj.f', 'obj.b.s', + 'obj.df.a', + 'obj.df.b[0].c', + 'obj.df.b[1]', + 'obj.df.d.e', + 'obj.ds.a', + 'obj.ds.b[0].c', + 'obj.ds.b[1]', + 'obj.ds.d.e', 'arrF[0].never', 'arrB[0].never', 'arrS[0].never', @@ -107,7 +124,7 @@ describe('filterByVisibility', () => { data: { arr: ['f'], objArr: [{ f: 1 }, { f: 4 }], - obj: { f: 'a' }, + obj: { f: 'a', df: { a: 1, b: [{ c: 1 }, '1'], d: { e: 'x' } } }, arrF: [], objF: {}, arrU: ['f', 'b'], @@ -121,6 +138,10 @@ describe('filterByVisibility', () => { 'objArr[1].b', 'objArr[1].s', 'obj.b.s', + 'obj.ds.a', + 'obj.ds.b[0].c', + 'obj.ds.b[1]', + 'obj.ds.d.e', 'arrF[0].never', 'arrB[0].never', 'arrS[0].never', @@ -137,7 +158,7 @@ describe('filterByVisibility', () => { arr: ['b'], arrU: ['t'], objArr: [{ b: 2 }, { b: 5 }], - obj: { b: {} }, + obj: { b: {}, df: {}, ds: {} }, arrF: [{ never: 'here' }], arrB: [{ never: 'here' }], arrS: [{ never: 'here' }], @@ -156,6 +177,14 @@ describe('filterByVisibility', () => { 'objArr[1].s', 'obj.f', 'obj.b.s', + 'obj.df.a', + 'obj.df.b[0].c', + 'obj.df.b[1]', + 'obj.df.d.e', + 'obj.ds.a', + 'obj.ds.b[0].c', + 'obj.ds.b[1]', + 'obj.ds.d.e', ], }, ], @@ -165,7 +194,10 @@ describe('filterByVisibility', () => { data: { arr: ['s'], objArr: [{ s: 3 }, { s: 6 }], - obj: { b: { s: true } }, + obj: { + b: { s: true }, + ds: { a: 1, b: [{ c: 1 }, '1'], d: { e: 'x' } }, + }, arrS: [], objS: {}, }, @@ -180,6 +212,10 @@ describe('filterByVisibility', () => { 'objArr[1].f', 'objArr[1].b', 'obj.f', + 'obj.df.a', + 'obj.df.b[0].c', + 'obj.df.b[1]', + 'obj.df.d.e', 'arrF[0].never', 'arrB[0].never', 'arrS[0].never', @@ -196,6 +232,7 @@ describe('filterByVisibility', () => { data, filter, visibility, + deepVisibility, deprecations, undefined, true, @@ -210,6 +247,7 @@ describe('filterByVisibility', () => { data, [], visibility, + deepVisibility, deprecations, undefined, true, diff --git a/packages/config-loader/src/schema/filtering.ts b/packages/config-loader/src/schema/filtering.ts index 1c2d0c122a..1b7bbf43c1 100644 --- a/packages/config-loader/src/schema/filtering.ts +++ b/packages/config-loader/src/schema/filtering.ts @@ -31,6 +31,7 @@ export function filterByVisibility( data: JsonObject, includeVisibilities: ConfigVisibility[], visibilityByDataPath: Map, + deepVisibilityByDataPath: Map, deprecationByDataPath: Map, transformFunc?: TransformFunc, withFilteredKeys?: boolean, @@ -47,11 +48,17 @@ export function filterByVisibility( jsonVal: JsonValue, visibilityPath: string, // Matches the format we get from ajv filterPath: string, // Matches the format of the ConfigReader + inheritedVisibility: ConfigVisibility, ): JsonValue | undefined { const visibility = - visibilityByDataPath.get(visibilityPath) ?? DEFAULT_CONFIG_VISIBILITY; + visibilityByDataPath.get(visibilityPath) ?? inheritedVisibility; const isVisible = includeVisibilities.includes(visibility); + // If a deep visibility is set for our current path, then we that as our + // default visibility for all children until we encounter a different deep visibility + const newInheritedVisibility = + deepVisibilityByDataPath.get(visibilityPath) ?? inheritedVisibility; + // deprecated keys are added regardless of visibility indicator const deprecation = deprecationByDataPath.get(visibilityPath); if (deprecation) { @@ -84,7 +91,12 @@ export function filterByVisibility( path = `${visibilityPath}/${index}`; } - const out = transform(value, path, `${filterPath}[${index}]`); + const out = transform( + value, + path, + `${filterPath}[${index}]`, + newInheritedVisibility, + ); if (out !== undefined) { arr.push(out); @@ -108,6 +120,7 @@ export function filterByVisibility( value, `${visibilityPath}/${key}`, filterPath ? `${filterPath}.${key}` : key, + newInheritedVisibility, ); if (out !== undefined) { outObj[key] = out; @@ -124,7 +137,8 @@ export function filterByVisibility( return { filteredKeys: withFilteredKeys ? filteredKeys : undefined, deprecatedKeys: withDeprecatedKeys ? deprecatedKeys : undefined, - data: (transform(data, '', '') as JsonObject) ?? {}, + data: + (transform(data, '', '', DEFAULT_CONFIG_VISIBILITY) as JsonObject) ?? {}, }; } diff --git a/packages/config-loader/src/schema/load.ts b/packages/config-loader/src/schema/load.ts index 8ef971184f..885da44116 100644 --- a/packages/config-loader/src/schema/load.ts +++ b/packages/config-loader/src/schema/load.ts @@ -122,6 +122,7 @@ export async function loadConfigSchema( data, visibility, result.visibilityByDataPath, + result.deepVisibilityByDataPath, result.deprecationByDataPath, valueTransform, withFilteredKeys, @@ -135,6 +136,7 @@ export async function loadConfigSchema( data, Array.from(CONFIG_VISIBILITIES), result.visibilityByDataPath, + result.deepVisibilityByDataPath, result.deprecationByDataPath, valueTransform, withFilteredKeys, diff --git a/packages/config-loader/src/schema/types.ts b/packages/config-loader/src/schema/types.ts index 95576cf5aa..dc8da64038 100644 --- a/packages/config-loader/src/schema/types.ts +++ b/packages/config-loader/src/schema/types.ts @@ -75,6 +75,13 @@ type ValidationResult = { */ visibilityByDataPath: Map; + /** + * The configuration deep visibilities that were discovered during validation. + * + * The path in the key uses the form `////` + */ + deepVisibilityByDataPath: Map; + /** * The configuration visibilities that were discovered during validation. * diff --git a/packages/config-loader/src/sources/ObservableConfigProxy.test.ts b/packages/config-loader/src/sources/ObservableConfigProxy.test.ts index d1fccf3e80..f072536489 100644 --- a/packages/config-loader/src/sources/ObservableConfigProxy.test.ts +++ b/packages/config-loader/src/sources/ObservableConfigProxy.test.ts @@ -25,21 +25,17 @@ describe('ObservableConfigProxy', () => { const sub = config.subscribe(fn); expect(config.getOptionalNumber('x')).toBe(undefined); - config.setConfig(new ConfigReader({})); - expect(fn).toHaveBeenCalledTimes(1); - expect(config.getOptionalNumber('x')).toBe(undefined); - config.setConfig(new ConfigReader({ x: 1 })); - expect(fn).toHaveBeenCalledTimes(2); + expect(fn).toHaveBeenCalledTimes(1); expect(config.getOptionalNumber('x')).toBe(1); config.setConfig(new ConfigReader({ x: 3 })); - expect(fn).toHaveBeenCalledTimes(3); + expect(fn).toHaveBeenCalledTimes(2); sub.unsubscribe(); expect(config.getOptionalNumber('x')).toBe(3); config.setConfig(new ConfigReader({ x: 5 })); - expect(fn).toHaveBeenCalledTimes(3); + expect(fn).toHaveBeenCalledTimes(2); expect(config.getOptionalNumber('x')).toBe(5); }); @@ -58,18 +54,10 @@ describe('ObservableConfigProxy', () => { expect(config2.getOptionalNumber('x')).toBe(undefined); expect(config3.getOptionalNumber('x')).toBe(undefined); - config1.setConfig(new ConfigReader({})); + config1.setConfig(new ConfigReader({ x: 1, a: { x: 2, b: { x: 3 } } })); expect(fn1).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(1); expect(fn3).toHaveBeenCalledTimes(1); - expect(config1.getOptionalNumber('x')).toBe(undefined); - expect(config2.getOptionalNumber('x')).toBe(undefined); - expect(config3.getOptionalNumber('x')).toBe(undefined); - - config1.setConfig(new ConfigReader({ x: 1, a: { x: 2, b: { x: 3 } } })); - expect(fn1).toHaveBeenCalledTimes(2); - expect(fn2).toHaveBeenCalledTimes(2); - expect(fn3).toHaveBeenCalledTimes(2); expect(config1.getNumber('x')).toBe(1); expect(config2.getNumber('x')).toBe(2); expect(config3.getNumber('x')).toBe(3); @@ -79,9 +67,9 @@ describe('ObservableConfigProxy', () => { sub3.unsubscribe(); config1.setConfig(new ConfigReader({ x: 4, a: { x: 5, b: { x: 6 } } })); - expect(fn1).toHaveBeenCalledTimes(2); - expect(fn2).toHaveBeenCalledTimes(2); - expect(fn3).toHaveBeenCalledTimes(2); + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn2).toHaveBeenCalledTimes(1); + expect(fn3).toHaveBeenCalledTimes(1); expect(config1.getNumber('x')).toBe(4); expect(config2.getNumber('x')).toBe(5); expect(config3.getNumber('x')).toBe(6); @@ -140,4 +128,43 @@ describe('ObservableConfigProxy', () => { 'Only the root config can be closed', ); }); + + it('should only notify subscribers when the config data actually changes (ignoring key order)', () => { + const config = ObservableConfigProxy.create(new AbortController()); + const fn = jest.fn(); + config.subscribe(fn); + + expect(config.getOptionalNumber('a')).toBe(undefined); + + config.setConfig(new ConfigReader({})); + expect(fn).toHaveBeenCalledTimes(0); + expect(config.getOptionalNumber('a')).toBe(undefined); + + config.setConfig( + new ConfigReader({ + a: 1, + b: 1, + }), + ); + expect(fn).toHaveBeenCalledTimes(1); + expect(config.getOptionalNumber('a')).toBe(1); + + config.setConfig( + new ConfigReader({ + b: 1, + a: 1, + }), + ); + expect(fn).toHaveBeenCalledTimes(1); + expect(config.getOptionalNumber('a')).toBe(1); + + config.setConfig( + new ConfigReader({ + b: 1, + a: 2, + }), + ); + expect(fn).toHaveBeenCalledTimes(2); + expect(config.getOptionalNumber('a')).toBe(2); + }); }); diff --git a/packages/config-loader/src/sources/ObservableConfigProxy.ts b/packages/config-loader/src/sources/ObservableConfigProxy.ts index 59be836160..3d2084beac 100644 --- a/packages/config-loader/src/sources/ObservableConfigProxy.ts +++ b/packages/config-loader/src/sources/ObservableConfigProxy.ts @@ -16,6 +16,7 @@ import { Config, ConfigReader } from '@backstage/config'; import { JsonValue } from '@backstage/types'; +import isEqual from 'lodash/isEqual'; export class ObservableConfigProxy implements Config { private config: Config = new ConfigReader({}); @@ -40,12 +41,22 @@ export class ObservableConfigProxy implements Config { if (this.parent) { throw new Error('immutable'); } + + // We only notify subscribers if the data contents of the config actually + // changed. If they didn't, there's no point in callers trying to re-read + // them. However we still want to replace the local config object, since its + // runtime implementation could be entirely different. + const changed = !isEqual(this.config.get(), config.get()); + this.config = config; - for (const subscriber of this.subscribers) { - try { - subscriber(); - } catch (error) { - console.error(`Config subscriber threw error, ${error}`); + + if (changed) { + for (const subscriber of this.subscribers) { + try { + subscriber(); + } catch (error) { + console.error(`Config subscriber threw error, ${error}`); + } } } } diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index b09076f773..29cf4e9831 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/core-app-api +## 1.10.0-next.0 + +### Minor Changes + +- 18619f793c94: Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. +- 18619f793c94: The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + ## 1.9.1 ### Patch Changes diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 5746598ffd..8b344e748c 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -56,6 +56,7 @@ import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import PropTypes from 'prop-types'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; @@ -113,7 +114,7 @@ export type ApiFactoryScope = 'default' | 'app' | 'static'; // @public export const ApiProvider: { - (props: PropsWithChildren): JSX.Element; + (props: PropsWithChildren): React_2.JSX.Element; propTypes: { apis: PropTypes.Validator< NonNullable< @@ -235,7 +236,7 @@ export type AppRouteBinder = < ) => void; // @public -export function AppRouter(props: AppRouterProps): JSX.Element; +export function AppRouter(props: AppRouterProps): React_2.JSX.Element; // @public export interface AppRouterProps { @@ -368,7 +369,9 @@ export type ErrorBoundaryFallbackProps = PropsWithChildren<{ }>; // @public -export const FeatureFlagged: (props: FeatureFlaggedProps) => JSX.Element; +export const FeatureFlagged: ( + props: FeatureFlaggedProps, +) => React_2.JSX.Element; // @public export type FeatureFlaggedProps = { @@ -535,10 +538,10 @@ export type OAuth2Session = { idToken: string; accessToken: string; scopes: Set; - expiresAt: Date; + expiresAt?: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; + backstageIdentity?: BackstageIdentityResponse; }; // @public diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index ff43d81329..82114fb59e 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "1.9.1", + "version": "1.10.0-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts index 6f941379a7..8ca818ebd8 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts @@ -76,6 +76,37 @@ describe('OAuth2', () => { ); }); + it('should forward backstage identity', async () => { + getSession = jest.fn().mockResolvedValue({ + providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, + backstageIdentity: { + token: 'a.b.c', + expiresAt: theFuture, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, + }, + }); + const oauth2 = OAuth2.create({ + configApi: configApi, + scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`), + oauthRequestApi: new MockOAuthApi(), + discoveryApi: UrlPatternDiscovery.compile('http://example.com'), + }); + + await expect(oauth2.getBackstageIdentity()).resolves.toEqual({ + token: 'a.b.c', + expiresAt: theFuture, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, + }); + }); + it('should get refreshed id token', async () => { getSession = jest.fn().mockResolvedValue({ providerInfo: { idToken: 'id-token', expiresAt: theFuture }, diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index fd40a9c8fb..6be7cfc89d 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -30,6 +30,7 @@ import { SessionState, SessionApi, BackstageIdentityApi, + BackstageUserIdentity, } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { OAuth2Session } from './types'; @@ -49,10 +50,14 @@ export type OAuth2Response = { accessToken: string; idToken: string; scope: string; - expiresInSeconds: number; + expiresInSeconds?: number; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; + backstageIdentity: { + token: string; + expiresInSeconds?: number; + identity: BackstageUserIdentity; + }; }; const DEFAULT_PROVIDER = { @@ -92,8 +97,11 @@ export default class OAuth2 environment, provider, oauthRequestApi: oauthRequestApi, - sessionTransform(res: OAuth2Response): OAuth2Session { - return { + sessionTransform({ + backstageIdentity, + ...res + }: OAuth2Response): OAuth2Session { + const session: OAuth2Session = { ...res, providerInfo: { idToken: res.providerInfo.idToken, @@ -102,11 +110,26 @@ export default class OAuth2 scopeTransform, res.providerInfo.scope, ), - expiresAt: new Date( - Date.now() + res.providerInfo.expiresInSeconds * 1000, - ), + expiresAt: res.providerInfo.expiresInSeconds + ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000) + : undefined, }, }; + if (backstageIdentity) { + // TODO(Rugvip): This fallback can be removed a few releases after 1.18. It's there to avoid + // breaking deployments that update their frontend before updating their backend. + const expInSec = + backstageIdentity.expiresInSeconds ?? + res.providerInfo.expiresInSeconds; + session.backstageIdentity = { + token: backstageIdentity.token, + identity: backstageIdentity.identity, + expiresAt: expInSec + ? new Date(Date.now() + expInSec * 1000) + : undefined, + }; + } + return session; }, popupOptions, }); @@ -116,9 +139,21 @@ export default class OAuth2 defaultScopes: new Set(defaultScopes), sessionScopes: (session: OAuth2Session) => session.providerInfo.scopes, sessionShouldRefresh: (session: OAuth2Session) => { - const expiresInSec = - (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000; - return expiresInSec < 60 * 5; + // TODO(Rugvip): Optimize to use separate checks for provider vs backstage session expiration + let min = Infinity; + if (session.providerInfo?.expiresAt) { + min = Math.min( + min, + (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000, + ); + } + if (session.backstageIdentity?.expiresAt) { + min = Math.min( + min, + (session.backstageIdentity.expiresAt.getTime() - Date.now()) / 1000, + ); + } + return min < 60 * 5; }, }); diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts index bd3bb95ba6..e51de63dbe 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts @@ -31,8 +31,8 @@ export type OAuth2Session = { idToken: string; accessToken: string; scopes: Set; - expiresAt: Date; + expiresAt?: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; + backstageIdentity?: BackstageIdentityResponse; }; diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index af5e2b8d6a..d67b4b587c 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/core-components +## 0.13.5-next.0 + +### Patch Changes + +- 47782f4bfa5b: Add loading indicator to Table +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/version-bridge@1.0.4 + ## 0.13.4 ### Patch Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index df34abdd03..54c26039a3 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -54,7 +54,9 @@ import { TooltipProps } from '@material-ui/core/Tooltip'; import { WithStyles } from '@material-ui/core/styles'; // @public -export function AlertDisplay(props: AlertDisplayProps): JSX.Element | null; +export function AlertDisplay( + props: AlertDisplayProps, +): React_2.JSX.Element | null; // @public export type AlertDisplayProps = { @@ -66,7 +68,7 @@ export type AlertDisplayProps = { }; // @public -export function Avatar(props: AvatarProps): JSX.Element; +export function Avatar(props: AvatarProps): React_2.JSX.Element; // @public (undocumented) export type AvatarClassKey = 'avatar'; @@ -94,7 +96,7 @@ export type BackstageOverrides = Overrides & { export type BoldHeaderClassKey = 'root' | 'title' | 'subheader'; // @public -export function BottomLink(props: BottomLinkProps): JSX.Element; +export function BottomLink(props: BottomLinkProps): React_2.JSX.Element; // @public (undocumented) export type BottomLinkClassKey = 'root' | 'boxTitle' | 'arrow'; @@ -109,7 +111,7 @@ export type BottomLinkProps = { // Warning: (ae-forgotten-export) The symbol "Props_19" needs to be exported by the entry point index.d.ts // // @public -export function Breadcrumbs(props: Props_19): JSX.Element; +export function Breadcrumbs(props: Props_19): React_2.JSX.Element; // @public (undocumented) export type BreadcrumbsClickableTextClassKey = 'root'; @@ -120,7 +122,7 @@ 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): JSX.Element; +export function BrokenImageIcon(props: IconComponentProps): React_2.JSX.Element; // @public @deprecated (undocumented) export const Button: (props: LinkButtonProps) => JSX.Element; @@ -134,22 +136,24 @@ export type CardActionsTopRightClassKey = 'root'; // Warning: (ae-forgotten-export) The symbol "CardTabProps" needs to be exported by the entry point index.d.ts // // @public -export function CardTab(props: PropsWithChildren): JSX.Element; +export function CardTab( + props: PropsWithChildren, +): React_2.JSX.Element; // @public (undocumented) export type CardTabClassKey = 'root' | 'selected'; // @public (undocumented) -export function CatalogIcon(props: IconComponentProps): JSX.Element; +export function CatalogIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) -export function ChatIcon(props: IconComponentProps): JSX.Element; +export function ChatIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) export type ClosedDropdownClassKey = 'icon'; // @public -export function CodeSnippet(props: CodeSnippetProps): JSX.Element; +export function CodeSnippet(props: CodeSnippetProps): React_2.JSX.Element; // @public export interface CodeSnippetProps { @@ -164,14 +168,16 @@ export interface CodeSnippetProps { // Warning: (ae-forgotten-export) The symbol "Props_13" needs to be exported by the entry point index.d.ts // // @public -export function Content(props: PropsWithChildren): JSX.Element; +export function Content( + props: PropsWithChildren, +): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ContentHeaderProps" needs to be exported by the entry point index.d.ts // // @public export function ContentHeader( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type ContentHeaderClassKey = @@ -182,7 +188,7 @@ export type ContentHeaderClassKey = | 'title'; // @public -export function CopyTextButton(props: CopyTextButtonProps): JSX.Element; +export function CopyTextButton(props: CopyTextButtonProps): React_2.JSX.Element; // @public export interface CopyTextButtonProps { @@ -193,7 +199,9 @@ export interface CopyTextButtonProps { } // @public -export function CreateButton(props: CreateButtonProps): JSX.Element | null; +export function CreateButton( + props: CreateButtonProps, +): React_2.JSX.Element | null; // @public export type CreateButtonProps = { @@ -204,12 +212,12 @@ export type CreateButtonProps = { export type CustomProviderClassKey = 'form' | 'button'; // @public (undocumented) -export function DashboardIcon(props: IconComponentProps): JSX.Element; +export function DashboardIcon(props: IconComponentProps): React_2.JSX.Element; // @public export function DependencyGraph( props: DependencyGraphProps, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type DependencyGraphDefaultLabelClassKey = 'text'; @@ -301,7 +309,7 @@ export namespace DependencyGraphTypes { // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const DismissableBanner: (props: Props) => JSX.Element; +export const DismissableBanner: (props: Props) => React_2.JSX.Element; // @public (undocumented) export type DismissableBannerClassKey = @@ -317,15 +325,15 @@ export type DismissableBannerClassKey = export type DismissbleBannerClassKey = DismissableBannerClassKey; // @public (undocumented) -export function DocsIcon(props: IconComponentProps): JSX.Element; +export function DocsIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) -export function EmailIcon(props: IconComponentProps): JSX.Element; +export function EmailIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_2" needs to be exported by the entry point index.d.ts // // @public -export function EmptyState(props: Props_2): JSX.Element; +export function EmptyState(props: Props_2): React_2.JSX.Element; // @public (undocumented) export type EmptyStateClassKey = 'root' | 'action' | 'imageContainer'; @@ -347,7 +355,7 @@ export type ErrorBoundaryProps = { // Warning: (ae-forgotten-export) The symbol "IErrorPageProps" needs to be exported by the entry point index.d.ts // // @public -export function ErrorPage(props: IErrorPageProps): JSX.Element; +export function ErrorPage(props: IErrorPageProps): React_2.JSX.Element; // @public (undocumented) export type ErrorPageClassKey = 'container' | 'title' | 'subtitle'; @@ -355,7 +363,7 @@ export type ErrorPageClassKey = 'container' | 'title' | 'subtitle'; // @public export function ErrorPanel( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type ErrorPanelClassKey = 'text' | 'divider'; @@ -382,18 +390,18 @@ export type FeatureCalloutCircleClassKey = // @public export function FeatureCalloutCircular( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type FiltersContainerClassKey = 'root' | 'title'; // @public -export function Gauge(props: GaugeProps): JSX.Element; +export function Gauge(props: GaugeProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_10" needs to be exported by the entry point index.d.ts // // @public -export function GaugeCard(props: Props_10): JSX.Element; +export function GaugeCard(props: Props_10): React_2.JSX.Element; // @public (undocumented) export type GaugeCardClassKey = 'root'; @@ -429,18 +437,20 @@ export type GaugePropsGetColorOptions = { }; // @public (undocumented) -export function GitHubIcon(props: IconComponentProps): JSX.Element; +export function GitHubIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) -export function GroupIcon(props: IconComponentProps): JSX.Element; +export function GroupIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_14" needs to be exported by the entry point index.d.ts // // @public -export function Header(props: PropsWithChildren): JSX.Element; +export function Header(props: PropsWithChildren): React_2.JSX.Element; // @public (undocumented) -export function HeaderActionMenu(props: HeaderActionMenuProps): JSX.Element; +export function HeaderActionMenu( + props: HeaderActionMenuProps, +): React_2.JSX.Element; // @public (undocumented) export type HeaderActionMenuItem = { @@ -471,7 +481,7 @@ export type HeaderClassKey = // Warning: (ae-forgotten-export) The symbol "Props_5" needs to be exported by the entry point index.d.ts // // @public -export function HeaderIconLinkRow(props: Props_5): JSX.Element; +export function HeaderIconLinkRow(props: Props_5): React_2.JSX.Element; // @public (undocumented) export type HeaderIconLinkRowClassKey = 'links'; @@ -479,7 +489,7 @@ export type HeaderIconLinkRowClassKey = 'links'; // Warning: (ae-forgotten-export) The symbol "HeaderLabelProps" needs to be exported by the entry point index.d.ts // // @public -export function HeaderLabel(props: HeaderLabelProps): JSX.Element; +export function HeaderLabel(props: HeaderLabelProps): React_2.JSX.Element; // @public (undocumented) export type HeaderLabelClassKey = 'root' | 'label' | 'value'; @@ -487,7 +497,7 @@ export type HeaderLabelClassKey = 'root' | 'label' | 'value'; // Warning: (ae-forgotten-export) The symbol "HeaderTabsProps" needs to be exported by the entry point index.d.ts // // @public -export function HeaderTabs(props: HeaderTabsProps): JSX.Element; +export function HeaderTabs(props: HeaderTabsProps): React_2.JSX.Element; // @public (undocumented) export type HeaderTabsClassKey = @@ -497,14 +507,14 @@ export type HeaderTabsClassKey = | 'tabRoot'; // @public (undocumented) -export function HelpIcon(props: IconComponentProps): JSX.Element; +export function HelpIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_6" needs to be exported by the entry point index.d.ts // // @public export function HorizontalScrollGrid( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type HorizontalScrollGridClassKey = @@ -563,16 +573,16 @@ export type InfoCardVariants = 'flex' | 'fullHeight' | 'gridItem'; // Warning: (ae-forgotten-export) The symbol "IntroCardProps" needs to be exported by the entry point index.d.ts // // @public -export function IntroCard(props: IntroCardProps): JSX.Element; +export function IntroCard(props: IntroCardProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ItemCardProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ItemCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated -export function ItemCard(props: ItemCardProps): JSX.Element; +export function ItemCard(props: ItemCardProps): React_2.JSX.Element; // @public -export function ItemCardGrid(props: ItemCardGridProps): JSX.Element; +export function ItemCardGrid(props: ItemCardGridProps): React_2.JSX.Element; // @public (undocumented) export type ItemCardGridClassKey = 'root'; @@ -585,7 +595,7 @@ export type ItemCardGridProps = Partial> & { }; // @public -export function ItemCardHeader(props: ItemCardHeaderProps): JSX.Element; +export function ItemCardHeader(props: ItemCardHeaderProps): React_2.JSX.Element; // @public (undocumented) export type ItemCardHeaderClassKey = 'root'; @@ -603,7 +613,7 @@ export type ItemCardHeaderProps = Partial> & { // Warning: (ae-missing-release-tag) "Lifecycle" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Lifecycle(props: Props_7): JSX.Element; +export function Lifecycle(props: Props_7): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "LifecycleClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -614,7 +624,7 @@ export type LifecycleClassKey = 'alpha' | 'beta'; // Warning: (ae-missing-release-tag) "LinearGauge" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function LinearGauge(props: Props_11): JSX.Element | null; +export function LinearGauge(props: Props_11): React_2.JSX.Element | null; // Warning: (ae-missing-release-tag) "Link" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -644,7 +654,7 @@ export type LinkProps = Omit & export type LoginRequestListItemClassKey = 'root'; // @public -export function LogViewer(props: LogViewerProps): JSX.Element; +export function LogViewer(props: LogViewerProps): React_2.JSX.Element; // @public export type LogViewerClassKey = @@ -691,7 +701,7 @@ export interface LogViewerProps { // Warning: (ae-missing-release-tag) "MarkdownContent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function MarkdownContent(props: Props_8): JSX.Element; +export function MarkdownContent(props: Props_8): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "MarkdownContentClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -727,7 +737,9 @@ export type MicDropClassKey = 'micDrop'; // Warning: (ae-missing-release-tag) "MissingAnnotationEmptyState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function MissingAnnotationEmptyState(props: Props_3): JSX.Element; +export function MissingAnnotationEmptyState( + props: Props_3, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "MissingAnnotationEmptyStateClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -735,7 +747,9 @@ export function MissingAnnotationEmptyState(props: Props_3): JSX.Element; export type MissingAnnotationEmptyStateClassKey = 'code'; // @public -export const MobileSidebar: (props: MobileSidebarProps) => JSX.Element | null; +export const MobileSidebar: ( + props: MobileSidebarProps, +) => React_2.JSX.Element | null; // @public export type MobileSidebarProps = { @@ -745,7 +759,7 @@ export type MobileSidebarProps = { // Warning: (ae-missing-release-tag) "OAuthRequestDialog" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function OAuthRequestDialog(_props: {}): JSX.Element; +export function OAuthRequestDialog(_props: {}): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "OAuthRequestDialogClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -765,7 +779,7 @@ export type OpenedDropdownClassKey = 'icon'; // Warning: (ae-missing-release-tag) "OverflowTooltip" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function OverflowTooltip(props: Props_9): JSX.Element; +export function OverflowTooltip(props: Props_9): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "OverflowTooltipClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -776,7 +790,7 @@ export type OverflowTooltipClassKey = 'container'; // Warning: (ae-missing-release-tag) "Page" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Page(props: Props_16): JSX.Element; +export function Page(props: Props_16): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "PageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -789,19 +803,19 @@ export type PageClassKey = 'root'; // @public (undocumented) export function PageWithHeader( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "Progress" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function Progress( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public export const ProxiedSignInPage: ( props: ProxiedSignInPageProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public export type ProxiedSignInPageProps = SignInPageProps & { @@ -812,7 +826,7 @@ export type ProxiedSignInPageProps = SignInPageProps & { // Warning: (ae-missing-release-tag) "ResponseErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function ResponseErrorPanel(props: ErrorPanelProps): JSX.Element; +export function ResponseErrorPanel(props: ErrorPanelProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "ResponseErrorPanelClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -822,12 +836,14 @@ export type ResponseErrorPanelClassKey = 'text' | 'divider'; // Warning: (ae-missing-release-tag) "RoutedTabs" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function RoutedTabs(props: { routes: SubRoute_2[] }): JSX.Element; +export function RoutedTabs(props: { + routes: SubRoute_2[]; +}): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "SelectProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function Select(props: SelectProps): JSX.Element; +export function Select(props: SelectProps): React_2.JSX.Element; // @public (undocumented) export type SelectClassKey = @@ -851,7 +867,7 @@ export type SelectItem = { }; // @public -export const Sidebar: (props: SidebarProps) => JSX.Element; +export const Sidebar: (props: SidebarProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SIDEBAR_INTRO_LOCAL_STORAGE" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -905,10 +921,10 @@ export const SidebarDivider: React_2.ComponentType< export type SidebarDividerClassKey = 'root'; // @public -export const SidebarExpandButton: () => JSX.Element | null; +export const SidebarExpandButton: () => React_2.JSX.Element | null; // @public -export const SidebarGroup: (props: SidebarGroupProps) => JSX.Element; +export const SidebarGroup: (props: SidebarGroupProps) => React_2.JSX.Element; // @public export interface SidebarGroupProps extends BottomNavigationActionProps { @@ -920,7 +936,7 @@ export interface SidebarGroupProps extends BottomNavigationActionProps { // Warning: (ae-missing-release-tag) "SidebarIntro" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SidebarIntro(_props: {}): JSX.Element | null; +export function SidebarIntro(_props: {}): React_2.JSX.Element | null; // @public (undocumented) export type SidebarIntroClassKey = @@ -967,7 +983,7 @@ export type SidebarOpenState = { export function SidebarOpenStateProvider(props: { children: ReactNode; value: SidebarOpenState; -}): JSX.Element; +}): React_2.JSX.Element; // @public (undocumented) export type SidebarOptions = { @@ -978,7 +994,7 @@ export type SidebarOptions = { // Warning: (ae-missing-release-tag) "SidebarPage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SidebarPage(props: SidebarPageProps): JSX.Element; +export function SidebarPage(props: SidebarPageProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SidebarPageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1011,7 +1027,7 @@ export type SidebarPinStateContextType = { export function SidebarPinStateProvider(props: { children: ReactNode; value: SidebarPinStateContextType; -}): JSX.Element; +}): React_2.JSX.Element; // @public (undocumented) export type SidebarProps = { @@ -1036,7 +1052,9 @@ export const SidebarScrollWrapper: React_2.ComponentType< // Warning: (ae-missing-release-tag) "SidebarSearchField" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SidebarSearchField(props: SidebarSearchFieldProps): JSX.Element; +export function SidebarSearchField( + props: SidebarSearchFieldProps, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SidebarSpace" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1067,12 +1085,14 @@ export const SidebarSpacer: React_2.ComponentType< export type SidebarSpacerClassKey = 'root'; // @public -export const SidebarSubmenu: (props: SidebarSubmenuProps) => JSX.Element; +export const SidebarSubmenu: ( + props: SidebarSubmenuProps, +) => React_2.JSX.Element; // @public export const SidebarSubmenuItem: ( props: SidebarSubmenuItemProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type SidebarSubmenuItemDropdownItem = { @@ -1100,7 +1120,7 @@ export type SidebarSubmenuProps = { // Warning: (ae-missing-release-tag) "SignInPage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SignInPage(props: Props_17): JSX.Element; +export function SignInPage(props: Props_17): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SignInPageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1123,7 +1143,7 @@ export type SignInProviderConfig = { // @public (undocumented) export function SimpleStepper( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SimpleStepperFooterClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1136,7 +1156,7 @@ export type SimpleStepperFooterClassKey = 'root'; // @public (undocumented) export function SimpleStepperStep( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SimpleStepperStepClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1146,7 +1166,9 @@ export type SimpleStepperStepClassKey = 'end'; // Warning: (ae-missing-release-tag) "StatusAborted" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusAborted(props: PropsWithChildren<{}>): JSX.Element; +export function StatusAborted( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1163,33 +1185,39 @@ export type StatusClassKey = // Warning: (ae-missing-release-tag) "StatusError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusError(props: PropsWithChildren<{}>): JSX.Element; +export function StatusError(props: PropsWithChildren<{}>): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusOK" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusOK(props: PropsWithChildren<{}>): JSX.Element; +export function StatusOK(props: PropsWithChildren<{}>): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusPending" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusPending(props: PropsWithChildren<{}>): JSX.Element; +export function StatusPending( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusRunning" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusRunning(props: PropsWithChildren<{}>): JSX.Element; +export function StatusRunning( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusWarning" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusWarning(props: PropsWithChildren<{}>): JSX.Element; +export function StatusWarning( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_12" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "StructuredMetadataTable" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StructuredMetadataTable(props: Props_12): JSX.Element; +export function StructuredMetadataTable(props: Props_12): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StructuredMetadataTableListClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1211,7 +1239,7 @@ export type SubmenuOptions = { // Warning: (ae-missing-release-tag) "SubvalueCell" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SubvalueCell(props: SubvalueCellProps): JSX.Element; +export function SubvalueCell(props: SubvalueCellProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SubvalueCellClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1222,7 +1250,7 @@ export type SubvalueCellClassKey = 'value' | 'subvalue'; // Warning: (ae-missing-release-tag) "SupportButton" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SupportButton(props: SupportButtonProps): JSX.Element; +export function SupportButton(props: SupportButtonProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SupportButtonClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1272,7 +1300,9 @@ export type Tab = { // Warning: (ae-missing-release-tag) "TabbedCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function TabbedCard(props: PropsWithChildren): JSX.Element; +export function TabbedCard( + props: PropsWithChildren, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "TabbedCardClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1283,7 +1313,7 @@ export type TabbedCardClassKey = 'root' | 'indicator'; // Warning: (ae-missing-release-tag) "TabbedLayout" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function TabbedLayout(props: PropsWithChildren<{}>): JSX.Element; +export function TabbedLayout(props: PropsWithChildren<{}>): React_2.JSX.Element; // @public (undocumented) export namespace TabbedLayout { @@ -1296,7 +1326,9 @@ export namespace TabbedLayout { // Warning: (ae-missing-release-tag) "Table" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Table(props: TableProps): JSX.Element; +export function Table( + props: TableProps, +): React_2.JSX.Element; // @public (undocumented) export namespace Table { @@ -1356,6 +1388,8 @@ export interface TableProps // (undocumented) initialState?: TableState; // (undocumented) + isLoading?: boolean; + // (undocumented) onStateChange?: (state: TableState) => any; // (undocumented) subtitle?: string; @@ -1383,7 +1417,7 @@ export function TrendLine( Pick & { title?: string; }, -): JSX.Element | null; +): React_2.JSX.Element | null; // @public export function useContent(): { @@ -1401,7 +1435,7 @@ export function useQueryParamState( ): [T | undefined, SetQueryParams]; // @public (undocumented) -export function UserIcon(props: IconComponentProps): JSX.Element; +export function UserIcon(props: IconComponentProps): React_2.JSX.Element; // @public export class UserIdentity implements IdentityApi { @@ -1447,13 +1481,13 @@ export const useSidebarPinState: () => SidebarPinState; export function useSupportConfig(): SupportConfig; // @public (undocumented) -export function WarningIcon(props: IconComponentProps): JSX.Element; +export function WarningIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "WarningProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "WarningPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function WarningPanel(props: WarningProps): JSX.Element; +export function WarningPanel(props: WarningProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "WarningPanelClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 641a4e7270..d42f95de94 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.13.4", + "version": "0.13.5-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx index 7ebe15c931..db7c53897d 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx @@ -21,7 +21,7 @@ import { StructuredMetadataTable } from './StructuredMetadataTable'; describe('', () => { it('renders without exploding', () => { - const metadata = { hello: 'world' }; + const metadata = { hello: 'world', foo: null }; const { getByText } = render( , ); diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx index 6114054bbe..2b18f6ad19 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx @@ -113,7 +113,7 @@ function toValue( return {value}; } - if (typeof value === 'object' && !Array.isArray(value)) { + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { return renderMap(value, options, nested); } diff --git a/packages/core-components/src/components/Table/Table.stories.tsx b/packages/core-components/src/components/Table/Table.stories.tsx index 807d58057d..b38941012c 100644 --- a/packages/core-components/src/components/Table/Table.stories.tsx +++ b/packages/core-components/src/components/Table/Table.stories.tsx @@ -89,6 +89,43 @@ export const DefaultTable = () => { ); }; +export const LoadingTable = () => { + const classes = useStyles(); + const columns: TableColumn[] = [ + { + title: 'Column 1', + field: 'col1', + highlight: true, + }, + { + title: 'Column 2', + field: 'col2', + }, + { + title: 'Numeric value', + field: 'number', + type: 'numeric', + }, + { + title: 'A Date', + field: 'date', + type: 'date', + }, + ]; + + return ( +
+ + + ); +}; + export const EmptyTable = () => { const classes = useStyles(); const columns: TableColumn[] = [ diff --git a/packages/core-components/src/components/Table/Table.test.tsx b/packages/core-components/src/components/Table/Table.test.tsx index ffdca26d98..43c8cfe41d 100644 --- a/packages/core-components/src/components/Table/Table.test.tsx +++ b/packages/core-components/src/components/Table/Table.test.tsx @@ -48,6 +48,11 @@ describe('
', () => { expect(rendered.getByText('second value, second row')).toBeInTheDocument(); }); + it('renders loading without exploding', async () => { + const rendered = await renderInTestApp(
); + expect(rendered.getByTestId('loading-indicator')).toBeInTheDocument(); + }); + describe('with style rows', () => { describe('with CSS Properties object', () => { const styledColumn2 = { diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx index 5cea3dd6b6..9f2b04c214 100644 --- a/packages/core-components/src/components/Table/Table.tsx +++ b/packages/core-components/src/components/Table/Table.tsx @@ -53,6 +53,7 @@ import React, { import { SelectProps } from '../Select/Select'; import { Filter, Filters, SelectedFilters, Without } from './Filters'; +import CircularProgress from '@material-ui/core/CircularProgress'; // Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51 const tableIcons: Icons = { @@ -236,6 +237,7 @@ export interface TableProps filters?: TableFilter[]; initialState?: TableState; emptyContent?: ReactNode; + isLoading?: boolean; onStateChange?: (state: TableState) => any; } @@ -309,6 +311,7 @@ export function Table(props: TableProps) { emptyContent, onStateChange, components, + isLoading: isLoading, ...restProps } = props; const tableClasses = useTableStyles(); @@ -470,6 +473,28 @@ export function Table(props: TableProps) { const columnCount = columns.length; const Body = useCallback( bodyProps => { + if (isLoading) { + return ( + + + + + + ); + } + if (emptyContent && hasNoRows) { return ( @@ -482,7 +507,7 @@ export function Table(props: TableProps) { return ; }, - [hasNoRows, emptyContent, columnCount], + [hasNoRows, emptyContent, columnCount, isLoading], ); return ( diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 62b7d0fbdb..2c4cbb42c2 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/core-plugin-api +## 1.6.0-next.0 + +### Minor Changes + +- 18619f793c94: Added the optional `expiresAt` field that may now be part of a `BackstageIdentityResponse`. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + ## 1.5.3 ### Patch Changes diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 5577d94359..930f1f979d 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -45,7 +45,7 @@ export const analyticsApiRef: ApiRef; export const AnalyticsContext: (options: { attributes: Partial; children: ReactNode; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export type AnalyticsContextValue = CommonAnalyticsContext & { @@ -214,6 +214,7 @@ export type BackstageIdentityApi = { // @public export type BackstageIdentityResponse = { token: string; + expiresAt?: Date; identity: BackstageUserIdentity; }; @@ -782,7 +783,9 @@ export function withApis( ): ( WrappedComponent: React_2.ComponentType, ) => { - (props: React_2.PropsWithChildren>): JSX.Element; + ( + props: React_2.PropsWithChildren>, + ): React_2.JSX.Element; displayName: string; }; ``` diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 138ad24249..39d63d3437 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "1.5.3", + "version": "1.6.0-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 62d0d5c810..d9aec2ca5a 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -226,6 +226,11 @@ export type BackstageIdentityResponse = { */ token: string; + /** + * The time at which the token expires. If not set, it can be assumed that the token does not expire. + */ + expiresAt?: Date; + /** * Identity information derived from the token. */ diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 9ddd48393b..8aaca6fea8 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/create-app +## 0.5.5-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.12 + ## 0.5.4 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 4f629d951e..838f84093f 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.5.4", + "version": "0.5.5-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 99fb4f1372..d2660b29e0 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/dev-utils +## 1.0.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 1.0.18 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 44b16b4a62..8009803668 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "1.0.18", + "version": "1.0.20-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index 6bf5527c4b..c437f756ba 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,14 @@ # e2e-test +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.5-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + ## 0.2.6 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 48d4643394..cdf07369f9 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.2.6", + "version": "0.2.7-next.0", "private": true, "backstage": { "role": "cli" @@ -35,6 +35,7 @@ "cross-fetch": "^3.1.5", "fs-extra": "10.1.0", "handlebars": "^4.7.3", + "mysql2": "^2.2.5", "pgtools": "^1.0.0", "puppeteer": "^17.0.0", "tree-kill": "^1.2.2" diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index b3fb8d9a3c..20b2753f6b 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -31,7 +31,10 @@ import { waitForExit, print, } from '../lib/helpers'; + +import mysql from 'mysql2/promise'; import pgtools from 'pgtools'; + import { findPaths } from '@backstage/cli-common'; // eslint-disable-next-line no-restricted-syntax @@ -66,9 +69,12 @@ export async function run() { print('Starting the app'); await testAppServe(pluginId, appDir); - if (Boolean(process.env.POSTGRES_USER)) { - print('Testing the PostgreSQL backend startup'); - await preCleanPostgres(); + if ( + Boolean(process.env.POSTGRES_USER) || + Boolean(process.env.MYSQL_CONNECTION) + ) { + print('Testing the database backend startup'); + await preCleanDatabase(); const appConfig = path.resolve(appDir, 'app-config.yaml'); const productionConfig = path.resolve(appDir, 'app-config.production.yaml'); await testBackendStart( @@ -79,7 +85,7 @@ export async function run() { productionConfig, ); } - print('Testing the SQLite backend startup'); + print('Testing the Database backend startup'); await testBackendStart(appDir); if (process.env.CI) { @@ -427,24 +433,39 @@ async function testAppServe(pluginId: string, appDir: string) { } /** Drops PG databases */ -async function dropDB(database: string) { - const config = { - host: process.env.POSTGRES_HOST, - port: process.env.POSTGRES_PORT, - user: process.env.POSTGRES_USER, - password: process.env.POSTGRES_PASSWORD, - }; - +async function dropDB(database: string, client: string) { try { - await pgtools.dropdb(config, database); + if (client === 'postgres') { + const config = { + host: process.env.POSTGRES_HOST, + port: process.env.POSTGRES_PORT, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + }; + await pgtools.dropdb(config, database); + } else if (client === 'mysql') { + const connectionString = process.env.MYSQL_CONNECTION ?? ''; + const connection = await mysql.createConnection(connectionString); + await connection.execute('DROP DATABASE ?', [database]); + } } catch (_) { - /* do nothing*/ + /* do nothing */ } } /** Clean remnants from prior e2e runs */ -async function preCleanPostgres() { +async function preCleanDatabase() { print('Dropping old DBs'); + if (Boolean(process.env.POSTGRES_HOST)) { + await dropClientDatabases('postgres'); + } + if (Boolean(process.env.MYSQL_CONNECTION)) { + await dropClientDatabases('mysql'); + } + print('Dropped DBs'); +} + +async function dropClientDatabases(client: string) { await Promise.all( [ 'catalog', @@ -454,9 +475,8 @@ async function preCleanPostgres() { 'proxy', 'techdocs', 'search', - ].map(name => dropDB(`backstage_plugin_${name}`)), + ].map(name => dropDB(`backstage_plugin_${name}`, client)), ); - print('Created DBs'); } /** diff --git a/packages/frontend-app-api/.eslintrc.js b/packages/frontend-app-api/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/frontend-app-api/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/frontend-app-api/README.md b/packages/frontend-app-api/README.md new file mode 100644 index 0000000000..5cc5073f61 --- /dev/null +++ b/packages/frontend-app-api/README.md @@ -0,0 +1,12 @@ +# @backstage/frontend-app-api + +_This package was created through the Backstage CLI_. + +## Installation + +Install the package via Yarn: + +```sh +cd # if within a monorepo +yarn add @backstage/frontend-app-api +``` diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md new file mode 100644 index 0000000000..bf7db4451b --- /dev/null +++ b/packages/frontend-app-api/api-report.md @@ -0,0 +1,14 @@ +## API Report File for "@backstage/frontend-app-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/frontend-plugin-api'; + +// @public (undocumented) +export function createApp(options: { plugins: BackstagePlugin[] }): { + createRoot(): JSX.Element; +}; +``` diff --git a/packages/frontend-app-api/config.d.ts b/packages/frontend-app-api/config.d.ts new file mode 100644 index 0000000000..81c24d1185 --- /dev/null +++ b/packages/frontend-app-api/config.d.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + app?: { + /** + * @deepVisibility frontend + */ + extensions?: + | string + | { + [extensionId: string]: + | boolean + | string + | { + at?: string; + extension?: string; + config?: unknown; + }; + }; + }; +} diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json new file mode 100644 index 0000000000..c085c6dba6 --- /dev/null +++ b/packages/frontend-app-api/package.json @@ -0,0 +1,44 @@ +{ + "name": "@backstage/frontend-app-api", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "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" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@testing-library/jest-dom": "^5.10.1" + }, + "configSchema": "config.d.ts", + "files": [ + "config.d.ts", + "dist" + ], + "dependencies": { + "@backstage/config": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/plugin-graphiql": "workspace:^", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + } +} diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx new file mode 100644 index 0000000000..46cd0a7aed --- /dev/null +++ b/packages/frontend-app-api/src/createApp.tsx @@ -0,0 +1,206 @@ +/* + * 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 React from 'react'; +import { Config, ConfigReader } from '@backstage/config'; +import { + ExtensionInstanceConfig, + BackstagePlugin, + coreExtensionData, +} from '@backstage/frontend-plugin-api'; +import { RouteExtension } from './extensions/RouteExtension'; +import { + createExtensionInstance, + ExtensionInstance, +} from './createExtensionInstance'; + +// Since we'll never merge arrays in config the config reader context +// isn't too much of a help. Fall back to manual config reading logic +// as the Config interface makes it quite hard for us otherwise. +function readAppExtensionConfigs( + rootConfig: Config, +): Partial[] { + const arr = rootConfig.getOptional('app.extensions'); + if (!Array.isArray(arr)) { + if (arr === undefined) { + return []; + } + // This will throw, and show which part of config had the wrong type + rootConfig.getConfigArray('app.extensions'); + return []; + } + + return arr.map((value, index) => { + function errorMsg(msg: string, key?: string, prop?: string) { + return `Invalid extension configuration at app.extensions[${index}]${ + key ? `[${key}]` : '' + }${prop ? `.${prop}` : ''}, ${msg}`; + } + + if (typeof value === 'string') { + return { id: value }; + } else if ( + typeof value !== 'object' || + value === null || + Array.isArray(value) + ) { + throw new Error(errorMsg('must be a string or an object')); + } + + const keys = Object.keys(value); + if (keys.length !== 1) { + const joinedKeys = `"${keys.join('", "')}"`; + throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`)); + } + + const key = keys[0]; + const obj = value[key]; + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) { + throw new Error(errorMsg('must be an object', key)); + } + const at = obj.at; + if (at !== undefined && typeof at !== 'string') { + throw new Error(errorMsg('must be a string', key, 'at')); + } + const extension = obj.extension; + if (extension !== undefined && typeof extension !== 'string') { + throw new Error(errorMsg('must be a string', key, 'extension')); + } + if (extension) { + throw new Error('TODO: implement extension resolution'); + } + return { id: key, at, config: obj.config /* validate later */ }; + }); +} + +/** @public */ +export function createApp(options: { plugins: BackstagePlugin[] }): { + createRoot(): JSX.Element; +} { + const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any); + + // pull in default extension instance from discovered packages + // apply config to adjust default extension instances and add more + const extensionInstanceConfigs = [ + ...options.plugins.flatMap(plugin => plugin.defaultExtensionInstances), + { + id: 'core.router', + at: 'root/default', + extension: RouteExtension, + config: undefined, + }, + ]; + + const appExtensionConfigs = readAppExtensionConfigs(appConfig); + for (const appExtensionConfig of appExtensionConfigs) { + const existingConfig = extensionInstanceConfigs.find( + e => e.id === appExtensionConfig.id, + ); + if (existingConfig) { + if (appExtensionConfig.at) { + existingConfig.at = appExtensionConfig.at; + } + if (appExtensionConfig.extension) { + // TODO: do we want to reset config here? it might be completely + // unrelated to the previous one + existingConfig.extension = appExtensionConfig.extension; + } + if (appExtensionConfig.config) { + // TODO: merge config? + existingConfig.config = appExtensionConfig.config; + } + } else if (appExtensionConfig.id) { + const { id, at, extension, config } = appExtensionConfig; + if (!at || !extension) { + throw new Error(`Extension ${appExtensionConfig.id} is incomplete`); + } + extensionInstanceConfigs.push({ id, at, extension, config }); + } + } + + // Create attachment map so that we can look attachments up during instance creation + const attachmentMap = new Map< + string, + Map + >(); + for (const instanceConfig of extensionInstanceConfigs) { + const [extensionId, pointId = 'default'] = instanceConfig.at.split('/'); + + let pointMap = attachmentMap.get(extensionId); + if (!pointMap) { + pointMap = new Map(); + attachmentMap.set(extensionId, pointMap); + } + + let instances = pointMap.get(pointId); + if (!instances) { + instances = []; + pointMap.set(pointId, instances); + } + + instances.push(instanceConfig); + } + + const instances = new Map(); + + function createInstance( + instanceConfig: ExtensionInstanceConfig, + ): ExtensionInstance { + const existingInstance = instances.get(instanceConfig.id); + if (existingInstance) { + return existingInstance; + } + + const attachments = Object.fromEntries( + Array.from(attachmentMap.get(instanceConfig.id)?.entries() ?? []).map( + ([inputName, attachmentConfigs]) => [ + inputName, + attachmentConfigs.map(createInstance), + ], + ), + ); + + return createExtensionInstance({ + id: instanceConfig.id, + config: instanceConfig.config, + extension: instanceConfig.extension, + attachments, + }); + } + + const rootConfigs = attachmentMap.get('root')?.get('default') ?? []; + const rootInstances = rootConfigs.map(instanceConfig => + createInstance(instanceConfig), + ); + + return { + createRoot() { + const rootComponents = rootInstances.map( + e => + e.data.get( + coreExtensionData.reactComponent.id, + ) as typeof coreExtensionData.reactComponent.T, + ); + return ( + <> + {rootComponents.map(Component => ( + + ))} + + ); + }, + }; +} diff --git a/packages/frontend-app-api/src/createExtensionInstance.ts b/packages/frontend-app-api/src/createExtensionInstance.ts new file mode 100644 index 0000000000..0b62df74ae --- /dev/null +++ b/packages/frontend-app-api/src/createExtensionInstance.ts @@ -0,0 +1,52 @@ +/* + * 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 { Extension } from '@backstage/frontend-plugin-api'; +import mapValues from 'lodash/mapValues'; + +/** @internal */ +export interface ExtensionInstance { + id: string; + data: Map; + $$type: 'extension-instance'; +} + +/** @internal */ +export function createExtensionInstance(options: { + id: string; + extension: Extension; + config: unknown; + attachments: Record; +}): ExtensionInstance { + const { extension, config, attachments } = options; + const extensionData = new Map(); + extension.factory({ + config, + bind: mapValues(extension.output, ref => { + return (value: unknown) => extensionData.set(ref.id, value); + }), + inputs: mapValues( + extension.inputs, + ({ extensionData: pointData }, inputName) => { + // TODO: validation + return (attachments[inputName] ?? []).map(attachment => + mapValues(pointData, ref => attachment.data.get(ref.id)), + ); + }, + ), + }); + return { id: options.id, data: extensionData, $$type: 'extension-instance' }; +} diff --git a/packages/frontend-app-api/src/extensions/RouteExtension.tsx b/packages/frontend-app-api/src/extensions/RouteExtension.tsx new file mode 100644 index 0000000000..ef7e34ea30 --- /dev/null +++ b/packages/frontend-app-api/src/extensions/RouteExtension.tsx @@ -0,0 +1,53 @@ +/* + * 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 React from 'react'; +import { + createExtension, + coreExtensionData, +} from '@backstage/frontend-plugin-api'; +import { BrowserRouter, useRoutes } from 'react-router-dom'; + +export const RouteExtension = createExtension({ + inputs: { + routes: { + extensionData: { + path: coreExtensionData.routePath, + component: coreExtensionData.reactComponent, + }, + }, + }, + output: { + component: coreExtensionData.reactComponent, + }, + factory({ bind, inputs }) { + const Routes = () => { + const element = useRoutes( + inputs.routes.map(route => ({ + path: route.path, + element: , + })), + ); + + return element; + }; + bind.component(() => ( + + + + )); + }, +}); diff --git a/packages/frontend-app-api/src/index.ts b/packages/frontend-app-api/src/index.ts new file mode 100644 index 0000000000..45fe9413ba --- /dev/null +++ b/packages/frontend-app-api/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * TODO + * + * @packageDocumentation + */ + +export { createApp } from './createApp'; diff --git a/packages/frontend-app-api/src/setupTests.ts b/packages/frontend-app-api/src/setupTests.ts new file mode 100644 index 0000000000..c30f1d15cb --- /dev/null +++ b/packages/frontend-app-api/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; diff --git a/packages/frontend-plugin-api/.eslintrc.js b/packages/frontend-plugin-api/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/frontend-plugin-api/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/frontend-plugin-api/README.md b/packages/frontend-plugin-api/README.md new file mode 100644 index 0000000000..43cb4082dc --- /dev/null +++ b/packages/frontend-plugin-api/README.md @@ -0,0 +1,12 @@ +# @backstage/frontend-plugin-api + +_This package was created through the Backstage CLI_. + +## Installation + +Install the package via Yarn: + +```sh +cd # if within a monorepo +yarn add @backstage/frontend-plugin-api +``` diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md new file mode 100644 index 0000000000..4337b0b8c2 --- /dev/null +++ b/packages/frontend-plugin-api/api-report.md @@ -0,0 +1,124 @@ +## API Report File for "@backstage/frontend-plugin-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ComponentType } from 'react'; + +// @public (undocumented) +export type AnyExtensionDataMap = Record>; + +// @public (undocumented) +export interface BackstagePlugin { + // (undocumented) + $$type: 'backstage-plugin'; + // (undocumented) + defaultExtensionInstances: ExtensionInstanceConfig[]; + // (undocumented) + id: string; +} + +// @public (undocumented) +export interface BackstagePluginOptions { + // (undocumented) + defaultExtensionInstances?: ExtensionInstanceConfig[]; + // (undocumented) + id: string; +} + +// @public (undocumented) +export const coreExtensionData: { + reactComponent: ExtensionDataRef>; + routePath: ExtensionDataRef; +}; + +// @public (undocumented) +export function createExtension< + TData extends AnyExtensionDataMap, + TPoint extends Record< + string, + { + extensionData: AnyExtensionDataMap; + } + >, +>(options: CreateExtensionOptions): Extension; + +// @public (undocumented) +export interface CreateExtensionOptions< + TData extends AnyExtensionDataMap, + TPoint extends Record< + string, + { + extensionData: AnyExtensionDataMap; + } + >, +> { + // (undocumented) + factory(options: { + bind: ExtensionDataBind; + config?: unknown; + inputs: { + [pointName in keyof TPoint]: ExtensionDataValue< + TPoint[pointName]['extensionData'] + >[]; + }; + }): void; + // (undocumented) + inputs?: TPoint; + // (undocumented) + output: TData; +} + +// @public (undocumented) +export function createPlugin(options: BackstagePluginOptions): BackstagePlugin; + +// @public (undocumented) +export interface Extension { + // (undocumented) + $$type: 'extension'; + // (undocumented) + factory(options: { + bind: ExtensionDataBind; + config?: unknown; + inputs: Record>>; + }): void; + // (undocumented) + inputs: Record< + string, + { + extensionData: AnyExtensionDataMap; + } + >; + // (undocumented) + output: AnyExtensionDataMap; +} + +// @public (undocumented) +export type ExtensionDataBind = { + [K in keyof TData]: (value: TData[K]['T']) => void; +}; + +// @public (undocumented) +export type ExtensionDataRef = { + id: string; + T: T; + $$type: 'extension-data'; +}; + +// @public (undocumented) +export type ExtensionDataValue = { + [K in keyof TData]: TData[K]['T']; +}; + +// @public (undocumented) +export interface ExtensionInstanceConfig { + // (undocumented) + at: string; + // (undocumented) + config: unknown; + // (undocumented) + extension: Extension; + // (undocumented) + id: string; +} +``` diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json new file mode 100644 index 0000000000..731e66fed0 --- /dev/null +++ b/packages/frontend-plugin-api/package.json @@ -0,0 +1,38 @@ +{ + "name": "@backstage/frontend-plugin-api", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "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" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@testing-library/jest-dom": "^5.10.1" + }, + "files": [ + "dist" + ], + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts new file mode 100644 index 0000000000..e36391131d --- /dev/null +++ b/packages/frontend-plugin-api/src/index.ts @@ -0,0 +1,36 @@ +/* + * 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. + */ + +/** + * TODO + * + * @packageDocumentation + */ + +export { + createExtension, + coreExtensionData, + createPlugin, + type ExtensionInstanceConfig, + type BackstagePlugin, + type Extension, + type AnyExtensionDataMap, + type BackstagePluginOptions, + type CreateExtensionOptions, + type ExtensionDataBind, + type ExtensionDataRef, + type ExtensionDataValue, +} from './types'; diff --git a/packages/frontend-plugin-api/src/setupTests.ts b/packages/frontend-plugin-api/src/setupTests.ts new file mode 100644 index 0000000000..c30f1d15cb --- /dev/null +++ b/packages/frontend-plugin-api/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts new file mode 100644 index 0000000000..b2d131c62f --- /dev/null +++ b/packages/frontend-plugin-api/src/types.ts @@ -0,0 +1,116 @@ +/* + * 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 { ComponentType } from 'react'; + +/** @public */ +export type ExtensionDataRef = { + id: string; + T: T; + $$type: 'extension-data'; +}; + +/** @public */ +export function createExtensionDataRef(id: string): ExtensionDataRef { + return { id, $$type: 'extension-data' } as ExtensionDataRef; +} + +/** @public */ +export const coreExtensionData = { + reactComponent: createExtensionDataRef('core.reactComponent'), + routePath: createExtensionDataRef('core.routing.path'), +}; + +/** @public */ +export type AnyExtensionDataMap = Record>; + +/** @public */ +export type ExtensionDataBind = { + [K in keyof TData]: (value: TData[K]['T']) => void; +}; + +/** @public */ +export type ExtensionDataValue = { + [K in keyof TData]: TData[K]['T']; +}; + +/** @public */ +export interface CreateExtensionOptions< + TData extends AnyExtensionDataMap, + TPoint extends Record, +> { + inputs?: TPoint; + output: TData; + factory(options: { + bind: ExtensionDataBind; + config?: unknown; + inputs: { + [pointName in keyof TPoint]: ExtensionDataValue< + TPoint[pointName]['extensionData'] + >[]; + }; + }): void; +} + +/** @public */ +export interface Extension { + $$type: 'extension'; + inputs: Record; + output: AnyExtensionDataMap; + factory(options: { + bind: ExtensionDataBind; + config?: unknown; + inputs: Record>>; + }): void; +} + +/** @public */ +export function createExtension< + TData extends AnyExtensionDataMap, + TPoint extends Record, +>(options: CreateExtensionOptions): Extension { + return { ...options, $$type: 'extension', inputs: options.inputs ?? {} }; +} + +/** @public */ +export interface ExtensionInstanceConfig { + id: string; + at: string; + extension: Extension; + config: unknown; +} + +/** @public */ +export interface BackstagePluginOptions { + id: string; + defaultExtensionInstances?: ExtensionInstanceConfig[]; +} + +/** @public */ +export interface BackstagePlugin { + $$type: 'backstage-plugin'; + id: string; + defaultExtensionInstances: ExtensionInstanceConfig[]; +} + +/** @public */ +export function createPlugin(options: BackstagePluginOptions): BackstagePlugin { + return { + ...options, + $$type: 'backstage-plugin', + defaultExtensionInstances: options.defaultExtensionInstances ?? [], + }; +} diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 9d08c09bd9..51afad9159 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/integration-react +## 1.1.18-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + ## 1.1.16 ### Patch Changes diff --git a/packages/integration-react/api-report.md b/packages/integration-react/api-report.md index 3af0704990..c1a2e34907 100644 --- a/packages/integration-react/api-report.md +++ b/packages/integration-react/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { ApiFactory } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; @@ -13,6 +11,7 @@ import { Config } from '@backstage/config'; import { OAuthApi } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { SessionApi } from '@backstage/core-plugin-api'; @@ -108,7 +107,7 @@ export interface ScmAuthTokenResponse { // @public export const ScmIntegrationIcon: ( props: ScmIntegrationIconProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type ScmIntegrationIconProps = { diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index ec68a20f36..6f34e173d2 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "1.1.16", + "version": "1.1.18-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -53,7 +54,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index b4b97f579e..26ad2b1b94 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/integration +## 1.7.0-next.0 + +### Minor Changes + +- 5f1a92b9f19f: Added `AzureDevOpsCredentialsProvider` to support multiple Azure DevOps organizations and **deprecated** `AzureIntegrationConfig.credential` and `AzureIntegrationConfig.token` in favour of `AzureIntegrationConfig.credentials`. You can now use specific credentials for different Azure DevOps (Server) organizations by specifying the `organizations` field on a credential: + + ```yaml + integrations: + azure: + - host: dev.azure.com + credentials: + - organizations: + - my-org + - my-other-org + clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} + - organizations: + - yet-another-org + personalAccessToken: ${PERSONAL_ACCESS_TOKEN} + ``` + + See the [Azure integration documentation](https://backstage.io/docs/integrations/azure/locations) for more information. + +### Patch Changes + +- cb2e19d82d95: Gitiles: Fixed auth prefix issue +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + ## 1.6.0 ### Minor Changes diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 7266950ac4..f64c06257e 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -39,16 +39,58 @@ export type AwsS3IntegrationConfig = { }; // @public -export type AzureClientSecretCredential = { +export type AzureClientSecretCredential = AzureCredentialBase & { + kind: 'ClientSecret'; tenantId: string; clientId: string; clientSecret: string; }; // @public -export type AzureCredential = +export type AzureCredentialBase = { + kind: AzureDevOpsCredentialKind; + organizations?: string[]; +}; + +// @public +export type AzureDevOpsCredential = | AzureClientSecretCredential - | AzureManagedIdentityCredential; + | AzureManagedIdentityCredential + | PersonalAccessTokenCredential; + +// @public +export type AzureDevOpsCredentialKind = + | 'PersonalAccessToken' + | 'ClientSecret' + | 'ManagedIdentity'; + +// @public +export type AzureDevOpsCredentialLike = Omit< + Partial & + Partial & + Partial, + 'kind' +>; + +// @public +export type AzureDevOpsCredentials = { + headers: { + [name: string]: string; + }; + token: string; + type: AzureDevOpsCredentialType; +}; + +// @public +export interface AzureDevOpsCredentialsProvider { + // (undocumented) + getCredentials(opts: { + url: string; + }): Promise; +} + +// @public +export type AzureDevOpsCredentialType = 'bearer' | 'pat'; // @public export class AzureIntegration implements ScmIntegration { @@ -75,11 +117,13 @@ export class AzureIntegration implements ScmIntegration { export type AzureIntegrationConfig = { host: string; token?: string; - credential?: AzureCredential; + credential?: AzureDevOpsCredential; + credentials?: AzureDevOpsCredential[]; }; // @public -export type AzureManagedIdentityCredential = { +export type AzureManagedIdentityCredential = AzureCredentialBase & { + kind: 'ManagedIdentity'; clientId: string; }; @@ -180,6 +224,20 @@ export function buildGerritGitilesArchiveUrl( filePath: string, ): string; +// @public +export class DefaultAzureDevOpsCredentialsProvider + implements AzureDevOpsCredentialsProvider +{ + // (undocumented) + static fromIntegrations( + integrations: ScmIntegrationRegistry, + ): DefaultAzureDevOpsCredentialsProvider; + // (undocumented) + getCredentials(opts: { + url: string; + }): Promise; +} + // @public export class DefaultGithubCredentialsProvider implements GithubCredentialsProvider @@ -250,7 +308,7 @@ export function getAzureDownloadUrl(url: string): string; // @public export function getAzureFileFetchUrl(url: string): string; -// @public +// @public @deprecated export function getAzureRequestOptions( config: AzureIntegrationConfig, additionalHeaders?: Record, @@ -600,6 +658,12 @@ export function parseGerritGitilesUrl( // @public export function parseGerritJsonResponse(response: Response): Promise; +// @public +export type PersonalAccessTokenCredential = AzureCredentialBase & { + kind: 'PersonalAccessToken'; + personalAccessToken: string; +}; + // @public export function readAwsS3IntegrationConfig( config: Config, diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index a1eb7aceb9..9c08ec03a9 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -30,8 +30,38 @@ export interface Config { /** * Token used to authenticate requests. * @visibility secret + * @deprecated Use `credentials` instead. */ token?: string; + + /** + * The credential to use for requests. + * + * If no credential is specified anonymous access is used. + * + * @visibility secret + * @deprecated Use `credentials` instead. + */ + credential?: { + clientId?: string; + clientSecret?: string; + tenantId?: string; + personalAccessToken?: string; + }; + + /** + * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used. + * If not organization matches the first credential without an organization is used. + * + * If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used. + * @visibility secret + */ + credentials?: { + clientId?: string; + clientSecret?: string; + tenantId?: string; + personalAccessToken?: string; + }[]; }>; /** diff --git a/packages/integration/package.json b/packages/integration/package.json index b0da382abd..254538ab26 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration", "description": "Helpers for managing integrations towards external systems", - "version": "1.6.0", + "version": "1.7.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index d036d88c21..fe2cbf349c 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -29,6 +29,7 @@ export class AzureIntegration implements ScmIntegration { const configs = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); + return basicIntegrations( configs.map(c => new AzureIntegration(c)), i => i.config.host, diff --git a/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.test.ts b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.test.ts new file mode 100644 index 0000000000..c9fbb4c324 --- /dev/null +++ b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.test.ts @@ -0,0 +1,188 @@ +/* + * 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 { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; +import { + AccessToken, + ClientSecretCredential, + ManagedIdentityCredential, +} from '@azure/identity'; + +const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< + typeof ClientSecretCredential +>; + +const MockedManagedIdentityCredential = + ManagedIdentityCredential as jest.MockedClass< + typeof ManagedIdentityCredential + >; + +jest.mock('@azure/identity'); + +const seconds = (s: number) => s * 1000; +const minutes = (m: number) => seconds(60) * m; +const hours = (h: number) => minutes(60) * h; + +describe('CachedAzureDevOpsCredentialsProvider', () => { + beforeEach(() => { + jest.resetAllMocks(); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + hours(8), + token: 'fake-client-secret-token', + } as AccessToken), + ); + + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + hours(8), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + }); + + it('Should return a pat credential when a personal access token is configured', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'PersonalAccessToken', + personalAccessToken: 'token', + }); + const { headers, type, token } = await manager.getCredentials(); + + expect(headers).toStrictEqual({ + Authorization: `Basic ${btoa(`:${token}`)}`, + }); + + expect(type).toBe('pat'); + expect(token).toBe('token'); + }); + + it('Should return a bearer credential when a client secret is configured', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }); + + const { headers, type, token } = await manager.getCredentials(); + + expect(headers).toStrictEqual({ + Authorization: `Bearer fake-client-secret-token`, + }); + + expect(type).toBe('bearer'); + expect(token).toBe('fake-client-secret-token'); + }); + + it('Should return a bearer credential when a managed identity is configured', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ManagedIdentity', + clientId: 'id', + }); + + const { headers, type, token } = await manager.getCredentials(); + + expect(headers).toStrictEqual({ + Authorization: `Bearer fake-managed-identity-token`, + }); + + expect(type).toBe('bearer'); + expect(token).toBe('fake-managed-identity-token'); + }); + + it('Should not refresh the client secret token when it does not expire within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }); + + await manager.getCredentials(); + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedClientSecretCredential.prototype.getToken, + ).toHaveBeenCalledTimes(1); + }); + + it('Should return the managed identity token when it does not expire within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ManagedIdentity', + clientId: 'id', + }); + + await manager.getCredentials(); + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedManagedIdentityCredential.prototype.getToken, + ).toHaveBeenCalledTimes(1); + }); + + it('Should refresh the client secret token when it expires within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + minutes(9) + seconds(59), + token: 'fake-client-secret-token', + } as AccessToken), + ); + + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedClientSecretCredential.prototype.getToken, + ).toHaveBeenCalledTimes(2); + }); + + it('Should refresh the managed identity token when it expires within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ManagedIdentity', + clientId: 'id', + }); + + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + minutes(9) + seconds(59), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedManagedIdentityCredential.prototype.getToken, + ).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.ts b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.ts new file mode 100644 index 0000000000..aee4cefc41 --- /dev/null +++ b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.ts @@ -0,0 +1,127 @@ +/* + * 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 { AzureDevOpsCredential, PersonalAccessTokenCredential } from './config'; +import { + ClientSecretCredential, + ManagedIdentityCredential, + TokenCredential, +} from '@azure/identity'; +import { + AzureDevOpsCredentials, + AzureDevOpsCredentialsProvider, +} from './types'; + +type CachedAzureDevOpsCredentials = AzureDevOpsCredentials & { + expiresAt?: number; +}; + +function exhaustiveCheck(_param: never) {} + +const tenMinutes = 1000 * 60 * 10; + +/** + * A credentials provider that caches the credentials for as long as it is valid. + * + * @public + */ +export class CachedAzureDevOpsCredentialsProvider + implements AzureDevOpsCredentialsProvider +{ + azureDevOpsScope = '499b84ac-1321-427f-aa17-267ca6975798/.default'; + cached: CachedAzureDevOpsCredentials | undefined; + + static fromAzureDevOpsCredential( + credential: AzureDevOpsCredential, + ): CachedAzureDevOpsCredentialsProvider { + switch (credential.kind) { + case 'PersonalAccessToken': + return CachedAzureDevOpsCredentialsProvider.fromPersonalAccessTokenCredential( + credential, + ); + case 'ClientSecret': + return CachedAzureDevOpsCredentialsProvider.fromTokenCredential( + new ClientSecretCredential( + credential.tenantId, + credential.clientId, + credential.clientSecret, + ), + ); + case 'ManagedIdentity': + return CachedAzureDevOpsCredentialsProvider.fromTokenCredential( + new ManagedIdentityCredential(credential.clientId), + ); + default: + exhaustiveCheck(credential); + + throw new Error( + `Credential kind '${(credential as any).kind}' not supported`, + ); + } + } + + static fromTokenCredential( + credential: TokenCredential, + ): CachedAzureDevOpsCredentialsProvider { + return new CachedAzureDevOpsCredentialsProvider(credential); + } + + static fromPersonalAccessTokenCredential( + credential: PersonalAccessTokenCredential, + ) { + return new CachedAzureDevOpsCredentialsProvider( + credential.personalAccessToken, + ); + } + + private constructor(private readonly credential: TokenCredential | string) {} + + async getCredentials(): Promise { + if ( + this.cached === undefined || + (this.cached.expiresAt !== undefined && + Date.now() > this.cached.expiresAt) + ) { + if (typeof this.credential === 'string') { + this.cached = { + headers: { + Authorization: `Basic ${btoa(`:${this.credential}`)}`, + }, + type: 'pat', + token: this.credential, + }; + } else { + const accessToken = await this.credential.getToken( + this.azureDevOpsScope, + ); + + if (!accessToken) { + throw new Error('Failed to retrieve access token'); + } + + this.cached = { + expiresAt: accessToken.expiresOnTimestamp - tenMinutes, + headers: { + Authorization: `Bearer ${accessToken.token}`, + }, + type: 'bearer', + token: accessToken.token, + }; + } + } + + return this.cached; + } +} diff --git a/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.test.ts b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.test.ts new file mode 100644 index 0000000000..8f66d5742d --- /dev/null +++ b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.test.ts @@ -0,0 +1,465 @@ +/* + * 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 { ScmIntegrations } from '../ScmIntegrations'; +import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; +import { AzureDevOpsCredentialLike, AzureIntegrationConfig } from './config'; +import { ConfigReader } from '@backstage/config'; +import { DefaultAzureDevOpsCredentialsProvider } from './DefaultAzureDevOpsCredentialsProvider'; +import { + AccessToken, + ClientSecretCredential, + DefaultAzureCredential, + ManagedIdentityCredential, +} from '@azure/identity'; +import { DateTime } from 'luxon'; + +type AzureIntegrationConfigLike = Partial< + Omit +> & { + credential?: Partial; + credentials?: Partial[]; +}; + +const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< + typeof ClientSecretCredential +>; + +const MockedManagedIdentityCredential = + ManagedIdentityCredential as jest.MockedClass< + typeof ManagedIdentityCredential + >; + +const MockedDefaultAzureCredential = DefaultAzureCredential as jest.MockedClass< + typeof DefaultAzureCredential +>; + +jest.mock('@azure/identity'); + +describe('DefaultAzureDevOpsCredentialProvider', () => { + const buildProvider = (azureIntegrations: AzureIntegrationConfigLike[]) => + DefaultAzureDevOpsCredentialsProvider.fromIntegrations( + ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: azureIntegrations, + }, + }), + ), + ); + + beforeEach(() => { + jest.resetAllMocks(); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-client-secret-token', + } as AccessToken), + ); + + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + + MockedDefaultAzureCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-default-azure-credential-token', + } as AccessToken), + ); + + jest.spyOn( + CachedAzureDevOpsCredentialsProvider, + 'fromAzureDevOpsCredential', + ); + + jest.spyOn(CachedAzureDevOpsCredentialsProvider, 'fromTokenCredential'); + + jest.spyOn( + CachedAzureDevOpsCredentialsProvider, + 'fromPersonalAccessTokenCredential', + ); + }); + + describe('fromIntegrations', () => { + it('Should create a credential provider when a credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'pat', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential, + ).toHaveBeenCalledTimes(1); + }); + + it('Should create a single credential provider per credential', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'client-id-1', + }, + { + organizations: ['org3', 'org4'], + clientId: 'client-id-2', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential, + ).toHaveBeenCalledTimes(2); + }); + + it('Should create a default credential provider for Azure DevOps when no credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(1); + + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledWith(new DefaultAzureCredential()); + }); + + it('Should create a default credential provider for Azure DevOps when no default credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'client-id', + tenantId: 'tenant-id', + clientSecret: 'client-secret', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(2); + + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledWith(new DefaultAzureCredential()); + }); + + it('Should not create a default credential provider for Azure DevOps when another default credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + clientId: 'client-id', + tenantId: 'tenant-id', + clientSecret: 'client-secret', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(1); + + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledWith(expect.any(ClientSecretCredential)); + }); + + it('Should not create a default credential provider for on-premise Azure DevOps server when no credential is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential, + ).toHaveBeenCalledTimes(0); + + // expect 1 call because the Azure integration adds a default integration for dev.azure.com when it is not configured + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(1); + + expect( + CachedAzureDevOpsCredentialsProvider.fromPersonalAccessTokenCredential, + ).toHaveBeenCalledTimes(0); + }); + }); + + describe('getCredentials', () => { + describe('Azure DevOps (dev.azure.com)', () => { + it('Should return a token when a credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = provider.getCredentials({ + url: 'https://dev.azure.com/org1/project1', + }); + + expect(credentials).toBeDefined(); + }); + + it('Should return DefaultAzureCredential when no credential with the same organization is specified and host is dev.azure.com', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://dev.azure.com/org2/project1', + }); + + expect(credentials).toMatchObject({ + token: 'fake-default-azure-credential-token', + }); + }); + + it('Should return undefined when no credential is specified and host is Azure DevOps server', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://my.devops.server/org2/project1', + }); + + expect(credentials).toBeUndefined(); + }); + + it('Should prefer organization credential when a credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://dev.azure.com/org1/project1', + }); + + expect(credentials).toMatchObject({ + token: 'org1-pat', + }); + }); + + it('Should fallback to host credential when no credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://dev.azure.com/org2/project1', + }); + + expect(credentials).toMatchObject({ + token: 'fallback-pat', + }); + }); + }); + describe('Azure DevOps Server', () => { + [ + 'https://{host}/{organization}/{project}', + 'https://{host}/tfs/{organization}/{project}', + ].map(format => { + describe(`With url format ${format}`, () => { + const formatUrl = (opts: { + host: string; + organization: string; + project: string; + }) => + format + .replace('{host}', opts.host) + .replace('{organization}', opts.organization) + .replace('{project}', opts.project); + + it('Should return a token when a credential with the same host is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.devops.server', + organization: 'org1', + project: 'project1', + }), + }); + + expect(credentials).toBeDefined(); + }); + + it('Should prefer organization credential when a credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.devops.server', + organization: 'org1', + project: 'project1', + }), + }); + + expect(credentials).toMatchObject({ + token: 'org1-pat', + }); + }); + + it('Should fallback to host credential when no credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.devops.server', + organization: 'org2', + project: 'project1', + }), + }); + + expect(credentials).toMatchObject({ + token: 'fallback-pat', + }); + }); + + it('Should return a undefined when no credential with the same host is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.other.devops.server', + organization: 'org1', + project: 'project1', + }), + }); + + expect(credentials).toBeUndefined(); + }); + }); + }); + }); + }); +}); diff --git a/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.ts b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.ts new file mode 100644 index 0000000000..1ff35b0612 --- /dev/null +++ b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.ts @@ -0,0 +1,132 @@ +/* + * 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 { + AzureDevOpsCredentials, + AzureDevOpsCredentialsProvider, +} from './types'; +import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; +import { ScmIntegrationRegistry } from '../registry'; +import { DefaultAzureCredential } from '@azure/identity'; + +/** + * Default implementation of AzureDevOpsCredentialsProvider. + * @public + */ +export class DefaultAzureDevOpsCredentialsProvider + implements AzureDevOpsCredentialsProvider +{ + static fromIntegrations( + integrations: ScmIntegrationRegistry, + ): DefaultAzureDevOpsCredentialsProvider { + const providers = integrations.azure.list().reduce((acc, integration) => { + integration.config.credentials?.forEach(credential => { + if ( + credential.organizations === undefined || + credential.organizations.length === 0 + ) { + if (acc.get(integration.config.host) === undefined) { + acc.set( + integration.config.host, + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( + credential, + ), + ); + } + } else { + const provider = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( + credential, + ); + credential.organizations?.forEach(organization => { + acc.set(`${integration.config.host}/${organization}`, provider); + }); + } + }); + + if ( + integration.config.host === 'dev.azure.com' && + acc.get(integration.config.host) === undefined + ) { + acc.set( + integration.config.host, + CachedAzureDevOpsCredentialsProvider.fromTokenCredential( + new DefaultAzureCredential(), + ), + ); + } + + return acc; + }, new Map()); + + return new DefaultAzureDevOpsCredentialsProvider(providers); + } + + private constructor( + private readonly providers: Map< + string, + CachedAzureDevOpsCredentialsProvider + >, + ) {} + + private forAzureDevOpsServerOrganization( + url: URL, + ): AzureDevOpsCredentialsProvider | undefined { + const parts = url.pathname.split('/').filter(part => part !== ''); + if (url.host !== 'dev.azure.com' && parts.length > 0) { + if (parts[0] !== 'tfs') { + // url format: https://{host}/{organization} + return this.providers.get(`${url.host}/${parts[0]}`); + } else if (parts[0] === 'tfs' && parts.length > 1) { + // url format: https://{host}/tfs/{organization} + return this.providers.get(`${url.host}/${parts[1]}`); + } + } + + return undefined; + } + + private forAzureDevOpsOrganization( + url: URL, + ): AzureDevOpsCredentialsProvider | undefined { + const parts = url.pathname.split('/').filter(part => part !== ''); + if (url.host === 'dev.azure.com' && parts.length > 0) { + // url format: https://{host}/{organization} + return this.providers.get(`${url.host}/${parts[0]}`); + } + + return undefined; + } + + private forHost(url: URL): AzureDevOpsCredentialsProvider | undefined { + return this.providers.get(url.host); + } + + async getCredentials(opts: { + url: string; + }): Promise { + const url = new URL(opts.url); + const provider = + this.forAzureDevOpsOrganization(url) ?? + this.forAzureDevOpsServerOrganization(url) ?? + this.forHost(url); + + if (provider === undefined) { + return undefined; + } + + return provider.getCredentials(opts); + } +} diff --git a/packages/integration/src/azure/config.test.ts b/packages/integration/src/azure/config.test.ts index 54711e4ddb..eb36120c32 100644 --- a/packages/integration/src/azure/config.test.ts +++ b/packages/integration/src/azure/config.test.ts @@ -17,18 +17,30 @@ import { Config, ConfigReader } from '@backstage/config'; import { loadConfigSchema } from '@backstage/config-loader'; import { + AzureDevOpsCredentialLike, AzureIntegrationConfig, readAzureIntegrationConfig, readAzureIntegrationConfigs, } from './config'; +type AzureIntegrationConfigLike = Partial< + Omit +> & { + credential?: Partial; + credentials?: Partial[]; +}; + describe('readAzureIntegrationConfig', () => { - function buildConfig(data: Partial): Config { + const valid: any = { + host: 'dev.azure.com', + }; + + function buildConfig(data: AzureIntegrationConfigLike): Config { return new ConfigReader(data); } async function buildFrontendConfig( - data: Partial, + data: AzureIntegrationConfigLike, ): Promise { const fullSchema = await loadConfigSchema({ dependencies: ['@backstage/integration'], @@ -51,39 +63,155 @@ describe('readAzureIntegrationConfig', () => { return new ConfigReader((processed[0].data as any).integrations.azure[0]); } - it('reads all values when using a token', () => { - const output = readAzureIntegrationConfig( - buildConfig({ - host: 'a.com', - token: 't', - }), - ); - - expect(output).toEqual({ - host: 'a.com', - token: 't', - }); - }); - - it('reads all values when using a credential', () => { + it('reads all values when using a personal access token credential', () => { const output = readAzureIntegrationConfig( buildConfig({ host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenant', - }, + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 't', + }, + ], }), ); expect(output).toEqual({ host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenant', - }, + credentials: [ + { + kind: 'PersonalAccessToken', + organizations: ['org1'], + personalAccessToken: 't', + }, + ], + }); + }); + + it('reads all values when using a personal access token credential (without organizations)', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'a.com', + credentials: [ + { + personalAccessToken: 't', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'a.com', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: 't', + }, + ], + }); + }); + + it('reads all values when using a client secret credential', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + organizations: ['org1', 'org2'], + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }); + }); + + it('reads all values when using a client secret credential (without organizations)', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }); + }); + + it('reads all values when using a managed identity credential', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }); + }); + + it('reads all values when using a managed identity credential (without organizations)', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + clientId: 'id', + }, + ], }); }); @@ -92,96 +220,296 @@ describe('readAzureIntegrationConfig', () => { expect(output).toEqual({ host: 'dev.azure.com' }); }); - it('rejects funky configs', () => { - const valid: any = { - host: 'dev.azure.com', - }; + it('maps deprecated token to credentials', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + token: 't', + }), + ); + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: 't', + }, + ], + }); + }); + + it('maps deprecated credential to credentials', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credential: { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }, + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }, + ], + }); + }); + + it('rejects config when host is not valid', () => { expect(() => readAzureIntegrationConfig(buildConfig({ ...valid, host: 7 })), ).toThrow(/host/); + }); - expect(() => - readAzureIntegrationConfig(buildConfig({ ...valid, token: 7 })), - ).toThrow(/token/); - - expect(() => - readAzureIntegrationConfig(buildConfig({ ...valid, credential: 7 })), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { clientId: 7 } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { clientSecret: 7 } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { tenantId: 7 } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig(buildConfig({ ...valid, credential: {} })), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { clientSecret: 'secret' } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { tenantId: 'tenant' } }), - ), - ).toThrow(/credential/); - + it('rejects config when organizations is not valid', () => { expect(() => readAzureIntegrationConfig( buildConfig({ ...valid, - credential: { clientId: 'id', clientSecret: 'secret' }, + credentials: [ + { + organizations: [1, 2, 'org'], + }, + ], }), ), - ).toThrow(/credential/); + ).toThrow(/credentials/); + }); + it('rejects config when token is not valid', () => { expect(() => readAzureIntegrationConfig( buildConfig({ ...valid, - credential: { clientId: 'id', tenantId: 'tenant' }, + credentials: [ + { + personalAccessToken: 7, + }, + ], }), ), - ).toThrow(/credential/); + ).toThrow(/credentials/); + }); + it('rejects config when clientId is not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 7, + }, + ], + }), + ), + ).toThrow(/credentials/); + }); + + it('rejects config when clientSecret is not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 'id', + clientSecret: 7, + tenantId: 'tenant', + }, + ], + }), + ), + ).toThrow(/credentials/); + }); + + it('rejects config when tenantId is not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 7, + }, + ], + }), + ), + ).toThrow(/credentials/); + }); + + it('rejects config when at least one credential not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + personalAccessToken: 'token', + }, + { + clientId: 'id', + }, + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + { + clientId: 'id', + personalAccessToken: 'token', + }, + ], + }), + ), + ).toThrow(/not a valid credential/); + }); + + it('rejects config when using a client secret credential for Azure DevOps server', () => { expect(() => readAzureIntegrationConfig( buildConfig({ ...valid, host: 'a.com', + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }), + ), + ).toThrow(/hosts/); + }); + + it('rejects config when using a managed identity for Azure DevOps server', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + host: 'a.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }), + ), + ).toThrow(/personal access tokens/); + }); + + it('rejects config when using organizations for Azure DevOps server', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + host: 'a.com', + credentials: [ + { + clientId: 'id', + }, + ], + }), + ), + ).toThrow(/hosts/); + }); + + it('rejects config when both the credential and credentials field are specified', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, credential: { clientId: 'id', - tenantId: 'tenant', - clientSecret: 'secret', }, + credentials: [ + { + clientId: 'id', + }, + ], }), ), ).toThrow(/credential/); }); + it('rejects config when both the token and credentials field are specified', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + token: 'token', + credentials: [ + { + clientId: 'id', + }, + ], + }), + ), + ).toThrow(/token/); + }); + + it('rejects config when more than one credential does not specify an organization', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 'id', + }, + { + organizations: [], + personalAccessToken: 'pat', + }, + ], + }), + ), + ).toThrow(/organizations/); + }); + + it('rejects config when multiple credentials specify the same organization', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + { + organizations: ['org2', 'org3'], + personalAccessToken: 'pat', + }, + ], + }), + ), + ).toThrow(/organization org2/); + }); + it('works on the frontend', async () => { expect( readAzureIntegrationConfig( await buildFrontendConfig({ host: 'a.com', - token: 't', + credentials: [ + { + personalAccessToken: 't', + }, + ], }), ), ).toEqual({ @@ -191,46 +519,184 @@ describe('readAzureIntegrationConfig', () => { }); describe('readAzureIntegrationConfigs', () => { - function buildConfig(data: Partial[]): Config[] { + function buildConfig(data: AzureIntegrationConfigLike[]): Config[] { return data.map(item => new ConfigReader(item)); } - it('reads all values when using a token', () => { - const output = readAzureIntegrationConfigs( - buildConfig([ - { - host: 'a.com', - token: 't', - }, - ]), - ); - expect(output).toContainEqual({ - host: 'a.com', - token: 't', - }); - }); - - it('reads all values when using a credential', () => { + it('reads all values when using a personal access token credential', () => { const output = readAzureIntegrationConfigs( buildConfig([ { host: 'dev.azure.com', - credential: { + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 't', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'PersonalAccessToken', + organizations: ['org1'], + personalAccessToken: 't', + }, + ], + }, + ]); + }); + + it('reads all values when using a personal access token credential (without organizations)', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 't', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: 't', + }, + ], + }, + ]); + }); + + it('reads all values when using a client secret credential', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + organizations: ['org1', 'org2'], clientId: 'id', clientSecret: 'secret', tenantId: 'tenant', }, + ], + }, + ]); + }); + + it('reads all values when using a client secret credential (without organizations)', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], }, ]), ); - expect(output).toContainEqual({ - host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenant', + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], }, - }); + ]); + }); + + it('reads all values when using a managed identity credential', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }, + ]); + }); + + it('reads all values when using a managed identity credential (without organizations)', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + clientId: 'id', + }, + ], + }, + ]); }); it('adds a default entry when missing', () => { diff --git a/packages/integration/src/azure/config.ts b/packages/integration/src/azure/config.ts index 3f12f3ef13..98704ba570 100644 --- a/packages/integration/src/azure/config.ts +++ b/packages/integration/src/azure/config.ts @@ -36,6 +36,8 @@ export type AzureIntegrationConfig = { * The authorization token to use for requests. * * If no token is specified, anonymous access is used. + * + * @deprecated Use `credentials` instead. */ token?: string; @@ -43,15 +45,50 @@ export type AzureIntegrationConfig = { * The credential to use for requests. * * If no credential is specified anonymous access is used. + * + * @deprecated Use `credentials` instead. */ - credential?: AzureCredential; + credential?: AzureDevOpsCredential; + + /** + * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used. + * If not organization matches the first credential without an organization is used. + * + * If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used. + */ + credentials?: AzureDevOpsCredential[]; }; /** - * Authenticate using a client secret that was generated for an App Registration. + * The kind of Azure DevOps credential. * @public */ -export type AzureClientSecretCredential = { +export type AzureDevOpsCredentialKind = + | 'PersonalAccessToken' + | 'ClientSecret' + | 'ManagedIdentity'; + +/** + * Common fields for the Azure DevOps credentials. + * @public + */ +export type AzureCredentialBase = { + /** + * The kind of credential. + */ + kind: AzureDevOpsCredentialKind; + /** + * The Azure DevOps organizations for which to use this credential. + */ + organizations?: string[]; +}; + +/** + * A client secret credential that was generated for an App Registration. + * @public + */ +export type AzureClientSecretCredential = AzureCredentialBase & { + kind: 'ClientSecret'; /** * The Azure Active Directory tenant */ @@ -68,10 +105,11 @@ export type AzureClientSecretCredential = { }; /** - * Authenticate using a managed identity available at the deployment environment. + * A managed identity credential. * @public */ -export type AzureManagedIdentityCredential = { +export type AzureManagedIdentityCredential = AzureCredentialBase & { + kind: 'ManagedIdentity'; /** * The clientId */ @@ -79,33 +117,77 @@ export type AzureManagedIdentityCredential = { }; /** - * Credential used to authenticate to Azure Active Directory. + * A personal access token credential. * @public */ -export type AzureCredential = +export type PersonalAccessTokenCredential = AzureCredentialBase & { + kind: 'PersonalAccessToken'; + personalAccessToken: string; +}; + +/** + * The general shape of a credential that can be used to authenticate to Azure DevOps. + * @public + */ +export type AzureDevOpsCredentialLike = Omit< + Partial & + Partial & + Partial, + 'kind' +>; + +/** + * Credential used to authenticate to Azure DevOps. + * @public + */ +export type AzureDevOpsCredential = | AzureClientSecretCredential - | AzureManagedIdentityCredential; -export const isAzureClientSecretCredential = ( - credential: Partial, -): credential is AzureClientSecretCredential => { - const clientSecretCredential = credential as AzureClientSecretCredential; + | AzureManagedIdentityCredential + | PersonalAccessTokenCredential; - return ( - Object.keys(credential).length === 3 && - clientSecretCredential.clientId !== undefined && - clientSecretCredential.clientSecret !== undefined && - clientSecretCredential.tenantId !== undefined - ); -}; +const AzureDevOpsCredentialFields = [ + 'clientId', + 'clientSecret', + 'tenantId', + 'personalAccessToken', +] as const; +type AzureDevOpsCredentialField = (typeof AzureDevOpsCredentialFields)[number]; -export const isAzureManagedIdentityCredential = ( - credential: Partial, -): credential is AzureManagedIdentityCredential => { - return ( - Object.keys(credential).length === 1 && - (credential as AzureManagedIdentityCredential).clientId !== undefined - ); -}; +const AzureDevopsCredentialFieldMap = new Map< + AzureDevOpsCredentialKind, + AzureDevOpsCredentialField[] +>([ + ['ClientSecret', ['clientId', 'clientSecret', 'tenantId']], + ['ManagedIdentity', ['clientId']], + ['PersonalAccessToken', ['personalAccessToken']], +]); + +function asAzureDevOpsCredential( + credential: AzureDevOpsCredentialLike, +): AzureDevOpsCredential { + for (const entry of AzureDevopsCredentialFieldMap.entries()) { + const [kind, requiredFields] = entry; + + const forbiddenFields = AzureDevOpsCredentialFields.filter( + field => !requiredFields.includes(field as AzureDevOpsCredentialField), + ); + + if ( + requiredFields.every(field => credential[field] !== undefined) && + forbiddenFields.every(field => credential[field] === undefined) + ) { + return { + kind, + organizations: credential.organizations, + ...requiredFields.reduce((acc, field) => { + acc[field] = credential[field]; + return acc; + }, {} as Record), + } as AzureDevOpsCredential; + } + } + throw new Error('is not a valid credential'); +} /** * Reads a single Azure integration config. @@ -117,15 +199,62 @@ export function readAzureIntegrationConfig( config: Config, ): AzureIntegrationConfig { const host = config.getOptionalString('host') ?? AZURE_HOST; + + let credentialConfigs = config + .getOptionalConfigArray('credentials') + ?.map(credential => { + const result: Partial = { + organizations: credential.getOptionalStringArray('organizations'), + personalAccessToken: credential.getOptionalString( + 'personalAccessToken', + ), + tenantId: credential.getOptionalString('tenantId'), + clientId: credential.getOptionalString('clientId'), + clientSecret: credential.getOptionalString('clientSecret'), + }; + + return result; + }); + const token = config.getOptionalString('token'); - const credential = config.getOptional('credential') - ? { + if ( + config.getOptional('credential') !== undefined && + config.getOptional('credentials') !== undefined + ) { + throw new Error( + `Invalid Azure integration config, 'credential' and 'credentials' cannot be used together. Use 'credentials' instead.`, + ); + } + + if ( + config.getOptional('token') !== undefined && + config.getOptional('credentials') !== undefined + ) { + throw new Error( + `Invalid Azure integration config, 'token' and 'credentials' cannot be used together. Use 'credentials' instead.`, + ); + } + + if (token !== undefined) { + const mapped = [{ personalAccessToken: token }]; + credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped; + } + + if (config.getOptional('credential') !== undefined) { + const mapped = [ + { + organizations: config.getOptionalStringArray( + 'credential.organizations', + ), + token: config.getOptionalString('credential.token'), tenantId: config.getOptionalString('credential.tenantId'), clientId: config.getOptionalString('credential.clientId'), clientSecret: config.getOptionalString('credential.clientSecret'), - } - : undefined; + }, + ]; + credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped; + } if (!isValidHost(host)) { throw new Error( @@ -133,29 +262,92 @@ export function readAzureIntegrationConfig( ); } - if ( - credential && - !isAzureClientSecretCredential(credential) && - !isAzureManagedIdentityCredential(credential) - ) { - throw new Error( - `Invalid Azure integration config, credential is not valid`, + let credentials: AzureDevOpsCredential[] | undefined = undefined; + if (credentialConfigs !== undefined) { + const errors = credentialConfigs + ?.reduce((acc, credentialConfig, index) => { + let error: string | undefined = undefined; + try { + asAzureDevOpsCredential(credentialConfig); + } catch (e) { + error = e.message; + } + + if (error !== undefined) { + acc.push(`credential at position ${index + 1} ${error}`); + } + + return acc; + }, Array.of()) + .concat( + Object.entries( + credentialConfigs + .filter( + credential => + credential.organizations !== undefined && + credential.organizations.length > 0, + ) + .reduce((acc, credential, index) => { + credential.organizations?.forEach(organization => { + if (!acc[organization]) { + acc[organization] = []; + } + + acc[organization].push(index + 1); + }); + + return acc; + }, {} as Record), + ) + .filter(([_, indexes]) => indexes.length > 1) + .reduce((acc, [org, indexes]) => { + acc.push( + `organization ${org} is specified multiple times in credentials at positions ${indexes + .slice(0, indexes.length - 1) + .join(', ')} and ${indexes[indexes.length - 1]}`, + ); + return acc; + }, Array.of()), + ); + + if (errors?.length > 0) { + throw new Error( + `Invalid Azure integration config for ${host}: ${errors.join('; ')}`, + ); + } + + credentials = credentialConfigs.map(credentialConfig => + asAzureDevOpsCredential(credentialConfig), ); + + if ( + credentials.some( + credential => credential.kind !== 'PersonalAccessToken', + ) && + host !== AZURE_HOST + ) { + throw new Error( + `Invalid Azure integration config for ${host}, only personal access tokens can be used with hosts other than ${AZURE_HOST}`, + ); + } + + if ( + credentials.filter( + credential => + credential.organizations === undefined || + credential.organizations.length === 0, + ).length > 1 + ) { + throw new Error( + `Invalid Azure integration config for ${host}, you cannot specify multiple credentials without organizations`, + ); + } } - if (credential && host !== AZURE_HOST) { - throw new Error( - `Invalid Azure integration config, credential can only be used with ${AZURE_HOST}`, - ); - } - - if (credential && token) { - throw new Error( - `Invalid Azure integration config, specify either a token or a credential but not both`, - ); - } - - return { host, token, credential }; + return { + host, + credentials, + }; } /** diff --git a/packages/integration/src/azure/core.test.ts b/packages/integration/src/azure/core.test.ts index e6ef5401ac..601057e499 100644 --- a/packages/integration/src/azure/core.test.ts +++ b/packages/integration/src/azure/core.test.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - getAzureFileFetchUrl, - getAzureDownloadUrl, - getAzureRequestOptions, -} from './core'; +import { getAzureFileFetchUrl, getAzureDownloadUrl } from './core'; import { AccessToken, ClientSecretCredential, @@ -44,66 +40,6 @@ MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => ); describe('azure core', () => { - describe('getAzureRequestOptions', () => { - it('should not add authorization header when not using token or credential', async () => { - expect(await getAzureRequestOptions({ host: '' })).toEqual( - expect.objectContaining({ - headers: expect.not.objectContaining({ - Authorization: expect.anything(), - }), - }), - ); - }); - - it('should add authorization header when using a personal access token', async () => { - expect( - await getAzureRequestOptions({ host: '', token: '0123456789' }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Basic OjAxMjM0NTY3ODk=', - }), - }), - ); - }); - - it('should add authorization header when using a client secret', async () => { - expect( - await getAzureRequestOptions({ - host: '', - credential: { - clientId: 'fake-id', - clientSecret: 'fake-secret', - tenantId: 'fake-tenant', - }, - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer fake-client-secret-token', - }), - }), - ); - }); - - it('should add authorization header when using a managed identity', async () => { - expect( - await getAzureRequestOptions({ - host: '', - credential: { - clientId: 'fake-id', - }, - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer fake-managed-identity-token', - }), - }), - ); - }); - }); - describe('getAzureFileFetchUrl', () => { it.each([ { diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts index 31aa13d91f..1c25373962 100644 --- a/packages/integration/src/azure/core.ts +++ b/packages/integration/src/azure/core.ts @@ -15,15 +15,6 @@ */ import { AzureUrl } from './AzureUrl'; -import { - AzureIntegrationConfig, - isAzureManagedIdentityCredential, - isAzureClientSecretCredential, -} from './config'; -import { - ClientSecretCredential, - ManagedIdentityCredential, -} from '@azure/identity'; /** * Given a URL pointing to a file on a provider, returns a URL that is suitable @@ -62,45 +53,3 @@ export function getAzureDownloadUrl(url: string): string { export function getAzureCommitsUrl(url: string): string { return AzureUrl.fromRepoUrl(url).toCommitsUrl(); } - -/** - * Gets the request options necessary to make requests to a given provider. - * - * @param config - The relevant provider config - * @param additionalHeaders - Additional headers for the request - * @public - */ -export async function getAzureRequestOptions( - config: AzureIntegrationConfig, - additionalHeaders?: Record, -): Promise<{ headers: Record }> { - const azureDevOpsScope = '499b84ac-1321-427f-aa17-267ca6975798/.default'; - const headers: Record = additionalHeaders - ? { ...additionalHeaders } - : {}; - - const { token, credential } = config; - if (credential) { - if (isAzureClientSecretCredential(credential)) { - const servicePrincipal = new ClientSecretCredential( - credential.tenantId, - credential.clientId, - credential.clientSecret, - ); - - const accessToken = await servicePrincipal.getToken(azureDevOpsScope); - headers.Authorization = `Bearer ${accessToken.token}`; - } else if (isAzureManagedIdentityCredential(credential)) { - const managedIdentity = new ManagedIdentityCredential( - credential.clientId, - ); - const accessToken = await managedIdentity.getToken(azureDevOpsScope); - headers.Authorization = `Bearer ${accessToken.token}`; - } - } else if (token) { - const buffer = Buffer.from(`:${config.token}`, 'utf8'); - headers.Authorization = `Basic ${buffer.toString('base64')}`; - } - - return { headers }; -} diff --git a/packages/integration/src/azure/deprecated.test.ts b/packages/integration/src/azure/deprecated.test.ts new file mode 100644 index 0000000000..4411ef5f2f --- /dev/null +++ b/packages/integration/src/azure/deprecated.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { getAzureRequestOptions } from './deprecated'; +import { DateTime } from 'luxon'; +import { + AccessToken, + ClientSecretCredential, + ManagedIdentityCredential, +} from '@azure/identity'; + +jest.mock('@azure/identity'); + +const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< + typeof ClientSecretCredential +>; + +const MockedManagedIdentityCredential = + ManagedIdentityCredential as jest.MockedClass< + typeof ManagedIdentityCredential + >; + +describe('azure core', () => { + beforeEach(() => { + jest.resetAllMocks(); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-client-secret-token', + } as AccessToken), + ); + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + }); + + describe('getAzureRequestOptions', () => { + it('should not add authorization header when not using token or credential', async () => { + expect(await getAzureRequestOptions({ host: '' })).toEqual( + expect.objectContaining({ + headers: expect.not.objectContaining({ + Authorization: expect.anything(), + }), + }), + ); + }); + + it('should add authorization header when using a personal access token', async () => { + const pat = '0123456789'; + const encoded = Buffer.from(`:${pat}`).toString('base64'); + expect( + await getAzureRequestOptions({ + host: '', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: pat, + }, + ], + }), + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Basic ${encoded}`, + }), + }), + ); + }); + + it('should add authorization header when using a client secret', async () => { + expect( + await getAzureRequestOptions({ + host: '', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'fake-id', + clientSecret: 'fake-secret', + tenantId: 'fake-tenant', + }, + ], + }), + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer fake-client-secret-token', + }), + }), + ); + }); + + it('should add authorization header when using a managed identity', async () => { + expect( + await getAzureRequestOptions({ + host: '', + credentials: [ + { + kind: 'ManagedIdentity', + clientId: 'fake-id', + }, + ], + }), + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer fake-managed-identity-token', + }), + }), + ); + }); + }); +}); diff --git a/packages/integration/src/azure/deprecated.ts b/packages/integration/src/azure/deprecated.ts new file mode 100644 index 0000000000..fd19f3dc14 --- /dev/null +++ b/packages/integration/src/azure/deprecated.ts @@ -0,0 +1,61 @@ +/* + * 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 { AzureIntegrationConfig } from './config'; +import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; + +/** + * Gets the request options necessary to make requests to a given provider. + * + * @param config - The relevant provider config + * @param additionalHeaders - Additional headers for the request + * @public + * @deprecated Use {@link AzureDevOpsCredentialsProvider} instead. + */ +export async function getAzureRequestOptions( + config: AzureIntegrationConfig, + additionalHeaders?: Record, +): Promise<{ headers: Record }> { + const headers: Record = additionalHeaders + ? { ...additionalHeaders } + : {}; + + /* + * Since we do not have a way to determine which organization the request is for, + * we will use the first credential that does not have an organization specified. + */ + const credentialConfig = config.credentials?.filter( + credential => + credential.organizations === undefined || + credential.organizations.length === 0, + )[0]; + + if (credentialConfig) { + const credentialsProvider = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( + credentialConfig, + ); + const credentials = await credentialsProvider.getCredentials(); + + return { + headers: { + ...credentials?.headers, + ...headers, + }, + }; + } + + return { headers }; +} diff --git a/packages/integration/src/azure/index.ts b/packages/integration/src/azure/index.ts index 21cf069807..b5f5c0a316 100644 --- a/packages/integration/src/azure/index.ts +++ b/packages/integration/src/azure/index.ts @@ -21,13 +21,21 @@ export { } from './config'; export type { AzureIntegrationConfig, - AzureCredential, - AzureManagedIdentityCredential, + AzureDevOpsCredentialKind, + AzureCredentialBase, AzureClientSecretCredential, + AzureManagedIdentityCredential, + PersonalAccessTokenCredential, + AzureDevOpsCredentialLike, + AzureDevOpsCredential, } from './config'; export { getAzureCommitsUrl, getAzureDownloadUrl, getAzureFileFetchUrl, - getAzureRequestOptions, } from './core'; + +export * from './types'; +export { DefaultAzureDevOpsCredentialsProvider } from './DefaultAzureDevOpsCredentialsProvider'; + +export * from './deprecated'; diff --git a/packages/integration/src/azure/types.ts b/packages/integration/src/azure/types.ts new file mode 100644 index 0000000000..72b341432c --- /dev/null +++ b/packages/integration/src/azure/types.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The type of Azure DevOps credential, either bearer or pat. + * @public + */ +export type AzureDevOpsCredentialType = 'bearer' | 'pat'; + +/** + * A set of credentials for Azure DevOps. + * + * @public + */ +export type AzureDevOpsCredentials = { + headers: { [name: string]: string }; + token: string; + type: AzureDevOpsCredentialType; +}; + +/** + * This allows implementations to be provided to retrieve Azure DevOps credentials. + * + * @public + * + */ +export interface AzureDevOpsCredentialsProvider { + getCredentials(opts: { + url: string; + }): Promise; +} diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index c00bae384f..5fbb41763a 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -37,8 +37,19 @@ describe('gerrit core', () => { describe('buildGerritGitilesArchiveUrl', () => { const config: GerritIntegrationConfig = { host: 'gerrit.com', + baseUrl: 'https://gerrit.com', gitilesBaseUrl: 'https://gerrit.com/gitiles', }; + const configWithPath: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com/gerrit', + gitilesBaseUrl: 'https://gerrit.com/gerrit/plugins/gitiles', + }; + const configWithDedicatedGitiles: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com/gerrit', + gitilesBaseUrl: 'https://dedicated-gitiles-server.com/gerrit/gitiles', + }; it('can create an archive url for a branch', () => { expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '')).toEqual( 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz', @@ -67,6 +78,40 @@ describe('gerrit core', () => { 'https://gerrit.com/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', ); }); + it('can create an authenticated url when auth is enabled and an url-path is used', () => { + const authConfig = { + ...configWithPath, + username: 'username', + password: 'password', + }; + expect( + buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), + ).toEqual( + 'https://gerrit.com/gerrit/a/plugins/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', + ); + }); + it('Cannot build an authenticated url when a dedicated Gitiles server is used', () => { + const authConfig = { + ...configWithDedicatedGitiles, + username: 'username', + password: 'password', + }; + expect(() => + buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), + ).toThrow( + 'Since the baseUrl (Gerrit) is not part of the gitilesBaseUrl, an authentication URL could not be constructed.', + ); + }); + it('Build a non-authenticated url when a dedicated Gitiles server is used', () => { + const authConfig = { + ...configWithDedicatedGitiles, + }; + expect( + buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), + ).toEqual( + 'https://dedicated-gitiles-server.com/gerrit/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', + ); + }); }); describe('buildGerritGitilesUrl', () => { diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index 107d5a4469..b3bdbc30e4 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { trimStart } from 'lodash'; import { GerritIntegrationConfig } from './config'; @@ -156,10 +155,23 @@ export function getAuthenticationPrefix( export function getGitilesAuthenticationUrl( config: GerritIntegrationConfig, ): string { - const parsedUrl = new URL(config.gitilesBaseUrl!); - return `${parsedUrl.protocol}//${parsedUrl.host}${getAuthenticationPrefix( - config, - )}${parsedUrl.pathname.substring(1)}`; + if (!config.baseUrl || !config.gitilesBaseUrl) { + throw new Error( + 'Unexpected Gerrit config values. baseUrl or gitilesBaseUrl not set.', + ); + } + if (config.gitilesBaseUrl.startsWith(config.baseUrl)) { + return config.gitilesBaseUrl.replace( + config.baseUrl.concat('/'), + config.baseUrl.concat(getAuthenticationPrefix(config)), + ); + } + if (config.password) { + throw new Error( + 'Since the baseUrl (Gerrit) is not part of the gitilesBaseUrl, an authentication URL could not be constructed.', + ); + } + return config.gitilesBaseUrl!; } /** diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index ddebca0746..1850528b4b 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/repo-tools +## 0.3.4-next.0 + +### Patch Changes + +- 0109d3f11159: The `generate-catalog-info` command now uses the first listed `CODEOWNER` as Component owner when initially + creating the `catalog-info.yaml` file. It continues to allow any one listed `CODEOWNER` when updating + entity metadata. +- ec13d3e86028: Fixed a bug with the `generate-catalog-info` command that could cause the `--dry-run` flag to indicate changes to files when no changes would actually be made if the command were run without the flag. +- db60a16e0a54: Added a `--ci` flag to the `generate-catalog-info` command. This flag behaves similarly to the same flag on `api-reports`: if `catalog-info.yaml` files would have been added or modified, then the process exits with status code `1`, and instructions are printed. +- Updated dependencies + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + ## 0.3.3 ### Patch Changes diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index 9c87bd8dff..12d71f3be4 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -44,6 +44,7 @@ Usage: backstage-repo-tools generate-catalog-info [options] Options: --dry-run + --ci -h, --help ``` diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 82099ea621..632cafafc9 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/repo-tools", "description": "CLI for Backstage repo tooling ", - "version": "0.3.3", + "version": "0.3.4-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts b/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts index f485e872e9..25703e4fc2 100644 --- a/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts +++ b/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts @@ -59,7 +59,7 @@ export function getOwnerFromCodeowners( ): string { const relPath = relativePath('.', absPath); const possibleOwners = getPossibleCodeowners(codeowners, relPath); - const owner = possibleOwners.slice(-1)[0]; + const owner = possibleOwners[0]; if (!owner) { throw new Error(`${relPath} isn't owned by anyone in CODEOWNERS`); diff --git a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts index 08f73898c8..38bc3ab4b2 100644 --- a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts +++ b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts @@ -29,26 +29,34 @@ import { import { BackstagePackageJson, isBackstagePackage, + isRejected, + isFulfilled, readFile, writeFile, } from './utils'; import { CodeOwnersEntry } from 'codeowners-utils'; type CreateFixPackageInfoYamlsOptions = { + ci?: boolean; dryRun?: boolean; }; export default async (opts: CreateFixPackageInfoYamlsOptions) => { - const { dryRun = false } = opts; + const { dryRun = false, ci = false } = opts; const packages = await PackageGraph.listTargetPackages(); const codeowners = await loadCodeowners(); const limit = pLimit(10); - const results = await Promise.allSettled( + // If --ci is passed, don't make any changes to the file system; we're only + // interested in knowing if changes would have been made. + const isDryRun = ci ? true : dryRun; + const checkForChanges = ci; + + const results = await Promise.allSettled( packages.map(({ packageJson, dir }) => limit(async () => { if (!isBackstagePackage(packageJson)) { - return; + return ''; } // Check if there is already a corresponding catalog-info.yaml @@ -59,32 +67,30 @@ export default async (opts: CreateFixPackageInfoYamlsOptions) => { yamlString = await readFile(infoYamlPath, { encoding: 'utf-8' }); } catch (e) { if (e.code === 'ENOENT') { - await createCatalogInfoYaml({ + return await createCatalogInfoYaml({ yamlPath: infoYamlPath, packageJson, codeowners, - dryRun, + dryRun: isDryRun, }); - return; } throw e; } - await fixCatalogInfoYaml({ + return await fixCatalogInfoYaml({ yamlPath: infoYamlPath, packageJson, codeowners, yamlString, - dryRun, + dryRun: isDryRun, + ci, }); }), ), ); - const rejects = results.filter( - r => r.status === 'rejected', - ) as PromiseRejectedResult[]; + const rejects = results.filter(isRejected); if (rejects.length > 0) { // Problems encountered. Print details here. console.error( @@ -94,6 +100,25 @@ export default async (opts: CreateFixPackageInfoYamlsOptions) => { console.error(); process.exit(1); } + + if (checkForChanges) { + const instructions = results + .filter(isFulfilled) + .map(r => r.value) + .filter(r => r !== ''); + + // Non-empty instructions indicate changes would have been made. + if (instructions.length > 0) { + console.error( + '\ncatalog-info.yaml file(s) out of sync with CODEOWNERS and/or package.json (see instructions above)\n', + ); + process.exit(1); + } else { + console.error( + 'catalog-info.yaml file(s) in sync with CODEOWNERS and package.json', + ); + } + } }; type CreateOptions = { @@ -105,6 +130,7 @@ type CreateOptions = { type FixOptions = CreateOptions & { yamlString: string; + ci: boolean; }; type BackstagePackageEntity = Entity & { @@ -117,18 +143,23 @@ type BackstagePackageEntity = Entity & { }; }; -function createCatalogInfoYaml(options: CreateOptions) { +async function createCatalogInfoYaml(options: CreateOptions) { const { codeowners, dryRun, packageJson, yamlPath } = options; + const instruction = `Create ${relativePath('.', yamlPath)}`; const owner = getOwnerFromCodeowners(codeowners, yamlPath); const entity = createOrMergeEntity(packageJson, owner); - return dryRun - ? Promise.resolve(console.error(`Create ${relativePath('.', yamlPath)}`)) - : writeFile(yamlPath, YAML.dump(entity)); + if (dryRun) { + console.error(instruction); + } else { + await writeFile(yamlPath, YAML.dump(entity)); + } + + return instruction; } -function fixCatalogInfoYaml(options: FixOptions) { - const { codeowners, dryRun, packageJson, yamlPath, yamlString } = options; +async function fixCatalogInfoYaml(options: FixOptions) { + const { ci, codeowners, dryRun, packageJson, yamlPath, yamlString } = options; const possibleOwners = getPossibleCodeowners( codeowners, relativePath('.', yamlPath), @@ -149,19 +180,55 @@ function fixCatalogInfoYaml(options: FixOptions) { const badName = yamlJson.metadata.name !== safeName; const badType = yamlJson.spec?.type !== `backstage-${packageJson.backstage.role}`; - const badDesc = yamlJson.metadata.description !== packageJson.description; - if (badOwner || badTitle || badName || badType || badDesc) { + if (badOwner || badTitle || badName || badType) { const owner = badOwner ? getOwnerFromCodeowners(codeowners, yamlPath) : yamlJson.spec?.owner; const newJson = createOrMergeEntity(packageJson, owner, yamlJson); - return dryRun - ? Promise.resolve(console.error(`Update ${relativePath('.', yamlPath)}`)) - : writeFile(yamlPath, yamlOverwrite(yamlString, newJson)); + const instructions = [`Update ${relativePath('.', yamlPath)}`]; + + // Show more detailed instructions when --ci flag is set. + if (ci) { + if (badOwner) { + instructions.push( + ` spec.owner cannot be "${ + yamlJson.spec.owner + }" because it must be one of (${possibleOwners.join( + ', ', + )}) as listed in CODEOWNERS`, + ); + } + + if (badTitle) { + instructions.push( + ` metadata.title cannot be "${yamlJson.metadata.title}" because it must be exactly "${packageJson.name}", the package.json name`, + ); + } + + if (badName) { + instructions.push( + ` metadata.name cannot be "${yamlJson.metadata.name}" because it must be exactly "${safeName}", as derived from package.json name`, + ); + } + + if (badType) { + instructions.push( + ` spec.type cannot be "${yamlJson.spec.type}" because it must be exactly "backstage-${packageJson.backstage.role}", as derived from package.json backstage.role`, + ); + } + } + + if (dryRun) { + console.error(instructions.join('\n')); + } else { + await writeFile(yamlPath, yamlOverwrite(yamlString, newJson)); + } + + return instructions.join('\n'); } - return Promise.resolve(); + return ''; } /** diff --git a/packages/repo-tools/src/commands/generate-catalog-info/utils.ts b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts index f446f4e867..b00fbd57a8 100644 --- a/packages/repo-tools/src/commands/generate-catalog-info/utils.ts +++ b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts @@ -40,3 +40,11 @@ export function isBackstagePackage( (packageJson as any)?.backstage?.role !== 'undefined' ); } + +export const isRejected = ( + input: PromiseSettledResult, +): input is PromiseRejectedResult => input.status === 'rejected'; + +export const isFulfilled = ( + input: PromiseSettledResult, +): input is PromiseFulfilledResult => input.status === 'fulfilled'; diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 7aa574d291..c429a253f8 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -102,6 +102,10 @@ export function registerCommands(program: Command) { '--dry-run', 'Shows what would happen without actually writing any yaml.', ) + .option( + '--ci', + 'CI run checks that there are no changes to catalog-info.yaml files', + ) .description('Create or fix info yaml files for all backstage packages') .action( lazy(() => diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 6728d9de41..1acc43911c 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,24 @@ # techdocs-cli-embedded-app +## 0.2.86-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/plugin-techdocs@1.6.8-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + ## 0.2.85 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/catalog-info.yaml b/packages/techdocs-cli-embedded-app/catalog-info.yaml new file mode 100644 index 0000000000..27c9325210 --- /dev/null +++ b/packages/techdocs-cli-embedded-app/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: techdocs-cli-embedded-app + title: techdocs-cli-embedded-app +spec: + lifecycle: production + type: backstage-frontend + owner: techdocs-maintainers diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index c8097fca3b..7a63f1f43c 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.85", + "version": "0.2.86-next.0", "private": true, "backstage": { "role": "frontend" diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 8334aa54b6..1c4f98fbab 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,16 @@ # @techdocs/cli +## 1.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + ## 1.4.5 ### Patch Changes diff --git a/packages/techdocs-cli/catalog-info.yaml b/packages/techdocs-cli/catalog-info.yaml new file mode 100644 index 0000000000..da80a9be3d --- /dev/null +++ b/packages/techdocs-cli/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: techdocs-cli + title: '@techdocs/cli' + description: Utility CLI for managing TechDocs sites in Backstage. +spec: + lifecycle: production + type: backstage-cli + owner: techdocs-maintainers diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 3dc8eda540..0a63bcaa7e 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "1.4.5", + "version": "1.4.7-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index d3e28b30d2..4aacbd0d5f 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/test-utils +## 1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + ## 1.4.2 ### Patch Changes diff --git a/packages/test-utils/alpha-api-report.md b/packages/test-utils/alpha-api-report.md index 3e0c4aa6c2..c37d79279c 100644 --- a/packages/test-utils/alpha-api-report.md +++ b/packages/test-utils/alpha-api-report.md @@ -4,11 +4,12 @@ ```ts import { PropsWithChildren } from 'react'; +import { default as React_2 } from 'react'; // @alpha export const MockPluginProvider: ({ children, -}: PropsWithChildren<{}>) => JSX.Element; +}: PropsWithChildren<{}>) => React_2.JSX.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 01fc31c28f..d4b38a243e 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -26,6 +26,7 @@ import { JsonValue } from '@backstage/types'; import { MatcherFunction } from '@testing-library/react'; import { Observable } from '@backstage/types'; import { PermissionApi } from '@backstage/plugin-permission-react'; +import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RenderOptions } from '@testing-library/react'; @@ -214,7 +215,7 @@ export type SyncLogCollector = () => void; // @public export const TestApiProvider: ( props: TestApiProviderProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type TestApiProviderProps = { diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 73ef2c5268..af78ca9732 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "1.4.2", + "version": "1.4.3-next.0", "publishConfig": { "access": "public" }, diff --git a/plugins/adr-backend/CHANGELOG.md b/plugins/adr-backend/CHANGELOG.md index e038706836..15529727c2 100644 --- a/plugins/adr-backend/CHANGELOG.md +++ b/plugins/adr-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-adr-backend +## 0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-adr-common@0.2.14-next.0 + - @backstage/plugin-search-common@1.2.5 + ## 0.3.6 ### Patch Changes diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index c15eafd6cf..aefd37de6f 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr-backend", - "version": "0.3.6", + "version": "0.3.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/adr-common/CHANGELOG.md b/plugins/adr-common/CHANGELOG.md index dfe54806fe..f43397f73d 100644 --- a/plugins/adr-common/CHANGELOG.md +++ b/plugins/adr-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-adr-common +## 0.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-search-common@1.2.5 + ## 0.2.12 ### Patch Changes diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json index 25e02845ef..359800f3ee 100644 --- a/plugins/adr-common/package.json +++ b/plugins/adr-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-adr-common", "description": "Common functionalities for the adr plugin", - "version": "0.2.12", + "version": "0.2.14-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md index e25c80d4d2..8cc6e343fa 100644 --- a/plugins/adr/CHANGELOG.md +++ b/plugins/adr/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-adr +## 0.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-adr-common@0.2.14-next.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + ## 0.6.4 ### Patch Changes diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md index 4cec5047fc..6ed7b68196 100644 --- a/plugins/adr/api-report.md +++ b/plugins/adr/api-report.md @@ -12,6 +12,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; import { isAdrAvailable } from '@backstage/plugin-adr-common'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -76,7 +78,10 @@ export const adrPlugin: BackstagePlugin< // @public export const AdrReader: { - (props: { adr: string; decorators?: AdrContentDecorator[] }): JSX.Element; + (props: { + adr: string; + decorators?: AdrContentDecorator[]; + }): React_2.JSX.Element; decorators: Readonly<{ createRewriteRelativeLinksDecorator(): AdrContentDecorator; createRewriteRelativeEmbedsDecorator(): AdrContentDecorator; @@ -107,7 +112,7 @@ export type AdrSearchResultListItemProps = { export const EntityAdrContent: (props: { contentDecorators?: AdrContentDecorator[] | undefined; filePathFilterFn?: AdrFilePathFilterFn | undefined; -}) => JSX.Element; +}) => JSX_2.Element; export { isAdrAvailable }; ``` diff --git a/plugins/adr/package.json b/plugins/adr/package.json index 37bf13619a..14d24bd89d 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr", - "version": "0.6.4", + "version": "0.6.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md index 7eea9d8287..ef3b8c6955 100644 --- a/plugins/airbrake-backend/CHANGELOG.md +++ b/plugins/airbrake-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-airbrake-backend +## 0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + ## 0.2.21 ### Patch Changes diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json index 530de9a77a..41a41b76de 100644 --- a/plugins/airbrake-backend/package.json +++ b/plugins/airbrake-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake-backend", - "version": "0.2.21", + "version": "0.2.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index b4d5afccdc..1899344f67 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-airbrake +## 0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/dev-utils@1.0.20-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.3.21 ### Patch Changes diff --git a/plugins/airbrake/api-report.md b/plugins/airbrake/api-report.md index 773e2a029a..a04d57d7e9 100644 --- a/plugins/airbrake/api-report.md +++ b/plugins/airbrake/api-report.md @@ -3,10 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -19,7 +18,7 @@ export const airbrakePlugin: BackstagePlugin< >; // @public -export const EntityAirbrakeContent: () => JSX.Element; +export const EntityAirbrakeContent: () => React_2.JSX.Element; // @public export const isAirbrakeAvailable: (entity: Entity) => boolean; diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 5ccd8bd076..7c34d18bfb 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.3.21", + "version": "0.3.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -55,7 +56,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index cd7c7973b5..72c7e982db 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-allure +## 0.1.39-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.37 ### Patch Changes diff --git a/plugins/allure/api-report.md b/plugins/allure/api-report.md index c1ce499ab2..a192851f26 100644 --- a/plugins/allure/api-report.md +++ b/plugins/allure/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -22,7 +23,7 @@ export const allurePlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityAllureReportContent: () => JSX.Element; +export const EntityAllureReportContent: () => JSX_2.Element; // @public (undocumented) export const isAllureReportAvailable: (entity: Entity) => boolean; diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 881d4d0c9d..21bd295497 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-allure", "description": "A Backstage plugin that integrates with Allure", - "version": "0.1.37", + "version": "0.1.39-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -54,7 +55,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index b6ceb1c28d..4713cef84a 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-analytics-module-ga +## 0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + ## 0.1.32 ### Patch Changes diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 434f231545..dc67603b51 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga", - "version": "0.1.32", + "version": "0.1.33-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/analytics-module-ga4/CHANGELOG.md b/plugins/analytics-module-ga4/CHANGELOG.md index bc08d64809..f4d661ead5 100644 --- a/plugins/analytics-module-ga4/CHANGELOG.md +++ b/plugins/analytics-module-ga4/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-analytics-module-ga4 +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + ## 0.1.3 ### Patch Changes diff --git a/plugins/analytics-module-ga4/package.json b/plugins/analytics-module-ga4/package.json index 63f03e440b..5a31cc2e36 100644 --- a/plugins/analytics-module-ga4/package.json +++ b/plugins/analytics-module-ga4/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga4", - "version": "0.1.3", + "version": "0.1.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/analytics-module-newrelic-browser/CHANGELOG.md b/plugins/analytics-module-newrelic-browser/CHANGELOG.md index 3090cd3fc2..3b35139403 100644 --- a/plugins/analytics-module-newrelic-browser/CHANGELOG.md +++ b/plugins/analytics-module-newrelic-browser/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-analytics-module-newrelic-browser +## 0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + ## 0.0.1 ### Patch Changes diff --git a/plugins/analytics-module-newrelic-browser/package.json b/plugins/analytics-module-newrelic-browser/package.json index ba72a1628b..277e2aea76 100644 --- a/plugins/analytics-module-newrelic-browser/package.json +++ b/plugins/analytics-module-newrelic-browser/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-newrelic-browser", - "version": "0.0.1", + "version": "0.0.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index e23c4a7c0c..d5ecfe53cc 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-apache-airflow +## 0.2.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + ## 0.2.14 ### Patch Changes diff --git a/plugins/apache-airflow/api-report.md b/plugins/apache-airflow/api-report.md index b427a0148e..be2a208bd4 100644 --- a/plugins/apache-airflow/api-report.md +++ b/plugins/apache-airflow/api-report.md @@ -6,15 +6,16 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public export const ApacheAirflowDagTable: (props: { dagIds?: string[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const ApacheAirflowPage: () => JSX.Element; +export const ApacheAirflowPage: () => JSX_2.Element; // @public (undocumented) export const apacheAirflowPlugin: BackstagePlugin< diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index 281d49c781..aabeaeecc5 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.2.14", + "version": "0.2.15-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,6 +33,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "qs": "^6.10.1", "react-use": "^17.2.4" @@ -52,7 +53,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md b/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md index 87ee39981f..4c35a4eab9 100644 --- a/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md +++ b/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-api-docs-module-protoc-gen-doc +## 0.1.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency + ## 0.1.2 ### Patch Changes diff --git a/plugins/api-docs-module-protoc-gen-doc/api-report.md b/plugins/api-docs-module-protoc-gen-doc/api-report.md index 696fee2a20..836f8313d8 100644 --- a/plugins/api-docs-module-protoc-gen-doc/api-report.md +++ b/plugins/api-docs-module-protoc-gen-doc/api-report.md @@ -3,12 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// +import { default as React_2 } from 'react'; // @public (undocumented) export const grpcDocsApiWidget: { type: string; title: string; - component: (definition: string) => JSX.Element; + component: (definition: string) => React_2.JSX.Element; }; ``` diff --git a/plugins/api-docs-module-protoc-gen-doc/package.json b/plugins/api-docs-module-protoc-gen-doc/package.json index 2d79af78a3..01eb7fc9a9 100644 --- a/plugins/api-docs-module-protoc-gen-doc/package.json +++ b/plugins/api-docs-module-protoc-gen-doc/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs-module-protoc-gen-doc", "description": "Additional functionalities for the api-docs plugin that renders the output of the protoc-gen-doc", - "version": "0.1.2", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,6 +31,7 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "grpc-docs": "^1.1.2" }, "peerDependencies": { @@ -40,8 +41,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@testing-library/jest-dom": "^5.16.4", - "@types/react": "^16.13.1 || ^17.0.0" + "@testing-library/jest-dom": "^5.16.4" }, "files": [ "dist" diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index f84d57542b..b5bed8a9b2 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-api-docs +## 0.9.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.9.7 ### Patch Changes diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index a9b21664c3..59d490a1fe 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -11,6 +11,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogTableRow } from '@backstage/plugin-catalog'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableColumn } from '@backstage/core-components'; @@ -18,14 +19,14 @@ import { TableProps } from '@backstage/core-components'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; // @public (undocumented) -export const ApiDefinitionCard: () => JSX.Element; +export const ApiDefinitionCard: () => React_2.JSX.Element; // @public export function ApiDefinitionDialog(props: { open: boolean; entity: ApiEntity; onClose: () => void; -}): JSX.Element; +}): React_2.JSX.Element; // @public (undocumented) export type ApiDefinitionWidget = { @@ -62,20 +63,22 @@ export { apiDocsPlugin as plugin }; // @public export const ApiExplorerIndexPage: ( props: DefaultApiExplorerPageProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export const ApiExplorerPage: ( props: DefaultApiExplorerPageProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) -export const ApiTypeTitle: (props: { apiEntity: ApiEntity }) => JSX.Element; +export const ApiTypeTitle: (props: { + apiEntity: ApiEntity; +}) => React_2.JSX.Element; // @public (undocumented) export const AsyncApiDefinitionWidget: ( props: AsyncApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type AsyncApiDefinitionWidgetProps = { @@ -86,17 +89,17 @@ export type AsyncApiDefinitionWidgetProps = { export const ConsumedApisCard: (props: { variant?: InfoCardVariants; columns?: TableColumn[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const ConsumingComponentsCard: (props: { variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export const DefaultApiExplorerPage: ( props: DefaultApiExplorerPageProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type DefaultApiExplorerPageProps = { @@ -109,40 +112,40 @@ export type DefaultApiExplorerPageProps = { export function defaultDefinitionWidgets(): ApiDefinitionWidget[]; // @public (undocumented) -export const EntityApiDefinitionCard: () => JSX.Element; +export const EntityApiDefinitionCard: () => JSX_2.Element; // @public (undocumented) export const EntityConsumedApisCard: (props: { variant?: InfoCardVariants | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityConsumingComponentsCard: (props: { variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityHasApisCard: (props: { variant?: InfoCardVariants | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityProvidedApisCard: (props: { variant?: InfoCardVariants | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityProvidingComponentsCard: (props: { variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const GraphQlDefinitionWidget: ( props: GraphQlDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type GraphQlDefinitionWidgetProps = { @@ -153,12 +156,12 @@ export type GraphQlDefinitionWidgetProps = { export const HasApisCard: (props: { variant?: InfoCardVariants; columns?: TableColumn[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const OpenApiDefinitionWidget: ( props: OpenApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type OpenApiDefinitionWidgetProps = { @@ -168,7 +171,7 @@ export type OpenApiDefinitionWidgetProps = { // @public (undocumented) export const PlainApiDefinitionWidget: ( props: PlainApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type PlainApiDefinitionWidgetProps = { @@ -180,17 +183,17 @@ export type PlainApiDefinitionWidgetProps = { export const ProvidedApisCard: (props: { variant?: InfoCardVariants; columns?: TableColumn[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const ProvidingComponentsCard: (props: { variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const TrpcApiDefinitionWidget: ( props: TrpcApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type TrpcApiDefinitionWidgetProps = { diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 1fe4133515..fb25c05aba 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.9.7", + "version": "0.9.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md index 0833267e83..129c0ab103 100644 --- a/plugins/apollo-explorer/CHANGELOG.md +++ b/plugins/apollo-explorer/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-apollo-explorer +## 0.1.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + ## 0.1.14 ### Patch Changes diff --git a/plugins/apollo-explorer/api-report.md b/plugins/apollo-explorer/api-report.md index 66cc473d0b..2dd6c14a90 100644 --- a/plugins/apollo-explorer/api-report.md +++ b/plugins/apollo-explorer/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { JSONObject } from '@apollo/explorer/src/helpers/types'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -30,7 +31,7 @@ export const ApolloExplorerPage: (props: { } | undefined; }[]; -}) => JSX.Element; +}) => JSX_2.Element; // @public export const apolloExplorerPlugin: BackstagePlugin< diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json index aae09fe19a..ac49adbc3d 100644 --- a/plugins/apollo-explorer/package.json +++ b/plugins/apollo-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apollo-explorer", - "version": "0.1.14", + "version": "0.1.15-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,6 +34,7 @@ "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4", "use-deep-compare-effect": "^1.8.1" }, @@ -52,7 +53,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 2ac0068ccc..a9c5f559db 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-app-backend +## 0.3.50-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-app-node@0.1.2-next.0 + ## 0.3.48 ### Patch Changes diff --git a/plugins/app-backend/migrations/20211229105307_init.js b/plugins/app-backend/migrations/20211229105307_init.js index 29e37b35f7..25ed48c307 100644 --- a/plugins/app-backend/migrations/20211229105307_init.js +++ b/plugins/app-backend/migrations/20211229105307_init.js @@ -20,11 +20,20 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { - return knex.schema.createTable('static_assets_cache', table => { + const isMySQL = knex.client.config.client.includes('mysql'); + await knex.schema.createTable('static_assets_cache', table => { table.comment( 'A cache of static assets that where previously deployed and may still be lazy-loaded by clients', ); - table.text('path').primary().notNullable().comment('The path of the file'); + if (!isMySQL) { + table + .text('path') + .primary() + .notNullable() + .comment('The path of the file'); + } else { + table.text('path').notNullable().comment('The path of the file'); + } table .dateTime('last_modified_at') .defaultTo(knex.fn.now()) @@ -35,6 +44,12 @@ exports.up = async function up(knex) { table.binary('content').notNullable().comment('The asset content'); table.index('last_modified_at', 'static_asset_cache_last_modified_at_idx'); }); + // specifically for mysql specify a unique index up to 254 characters(mysql limit) + if (isMySQL) { + await knex.schema.raw( + 'create unique index static_assets_cache_path_idx on static_assets_cache(path(254));', + ); + } }; /** diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 05db6bb37d..7b12e98881 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.48", + "version": "0.3.50-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts b/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts index 8a0935d4e3..68fe4e2287 100644 --- a/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts +++ b/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts @@ -37,7 +37,7 @@ jest.setTimeout(60_000); describe('StaticAssetsStore', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); it.each(databases.eachSupportedId())( @@ -153,14 +153,18 @@ describe('StaticAssetsStore', () => { content: async () => Buffer.alloc(0), }, ]); - + // interval check for postgresql + let hourPast = `now() + interval '-3600 seconds'`; + if (knex.client.config.client.includes('mysql')) { + hourPast = `date_sub(now(), interval 3600 second)`; + } else if (knex.client.config.client.includes('sqlite3')) { + hourPast = `datetime('now', '-3600 seconds')`; + } // Rewrite modified time of "old" to be 1h in the past const updated = await knex('static_assets_cache') .where({ path: 'old' }) .update({ - last_modified_at: knex.client.config.client.includes('sqlite3') - ? knex.raw(`datetime('now', '-3600 seconds')`) - : knex.raw(`now() + interval '-3600 seconds'`), + last_modified_at: knex.raw(hourPast), }); expect(updated).toBe(1); diff --git a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts index 6905d63779..9f6ada717d 100644 --- a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts +++ b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts @@ -138,14 +138,20 @@ export class StaticAssetsStore implements StaticAssetProvider { */ async trimAssets(options: { maxAgeSeconds: number }) { const { maxAgeSeconds } = options; + let lastModifiedInterval = this.#db.raw( + `now() + interval '${-maxAgeSeconds} seconds'`, + ); + if (this.#db.client.config.client.includes('mysql')) { + lastModifiedInterval = this.#db.raw( + `date_sub(now(), interval ${maxAgeSeconds} second)`, + ); + } else if (this.#db.client.config.client.includes('sqlite3')) { + lastModifiedInterval = this.#db.raw(`datetime('now', ?)`, [ + `-${maxAgeSeconds} seconds`, + ]); + } await this.#db('static_assets_cache') - .where( - 'last_modified_at', - '<=', - this.#db.client.config.client.includes('sqlite3') - ? this.#db.raw(`datetime('now', ?)`, [`-${maxAgeSeconds} seconds`]) - : this.#db.raw(`now() + interval '${-maxAgeSeconds} seconds'`), - ) + .where('last_modified_at', '<=', lastModifiedInterval) .delete(); } } diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index 1a2fd84689..a523770140 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-app-node +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index 842086ecee..eace393b7c 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-node", "description": "Node.js library for the app plugin", - "version": "0.1.0", + "version": "0.1.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js b/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md new file mode 100644 index 0000000000..a69e2aefbf --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -0,0 +1,15 @@ +# @backstage/plugin-auth-backend-module-gcp-iap-provider + +## 0.1.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: New module for `@backstage/plugin-auth-backend` that adds a GCP IAP auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 diff --git a/plugins/auth-backend-module-gcp-iap-provider/README.md b/plugins/auth-backend-module-gcp-iap-provider/README.md new file mode 100644 index 0000000000..a5f3ce1d4c --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/README.md @@ -0,0 +1,5 @@ +# Auth Backend Module - GCP IAP Provider + +## Links + +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/auth-backend-module-gcp-iap-provider/api-report.md b/plugins/auth-backend-module-gcp-iap-provider/api-report.md new file mode 100644 index 0000000000..02b77cea36 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/api-report.md @@ -0,0 +1,46 @@ +## API Report File for "@backstage/plugin-auth-backend-module-gcp-iap-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { JsonPrimitive } from '@backstage/types'; +import { ProxyAuthenticator } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +export const authModuleGcpIapProvider: () => BackendFeature; + +// @public (undocumented) +export const gcpIapAuthenticator: ProxyAuthenticator< + { + jwtHeader: string; + tokenValidator: (token: string) => Promise; + }, + { + iapToken: GcpIapTokenInfo; + } +>; + +// @public +export type GcpIapResult = { + iapToken: GcpIapTokenInfo; +}; + +// @public +export namespace gcpIapSignInResolvers { + const emailMatchingUserEntityAnnotation: SignInResolverFactory< + GcpIapResult, + unknown + >; +} + +// @public +export type GcpIapTokenInfo = { + sub: string; + email: string; + [key: string]: JsonPrimitive; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts new file mode 100644 index 0000000000..945378ada6 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + auth?: { + providers?: { + /** + * Configuration for the Google Cloud Platform Identity-Aware Proxy (IAP) auth provider. + */ + gcpIap?: { + [authEnv: string]: { + /** + * The audience to use when validating incoming JWT tokens. + * See https://backstage.io/docs/auth/google/gcp-iap-auth + */ + audience: string; + + /** + * The name of the header to read the JWT token from, defaults to `'x-goog-iap-jwt-assertion'`. + */ + jwtHeader?: string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json new file mode 100644 index 0000000000..25884fed57 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider", + "description": "A GCP IAP auth provider module for the Backstage auth backend", + "version": "0.1.0-next.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" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-gcp-iap-provider" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "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" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/types": "workspace:^", + "google-auth-library": "^8.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "express": "^4.18.2", + "msw": "^1.0.0", + "supertest": "^6.1.3" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..51efbe1a04 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices } from '@backstage/backend-test-utils'; +import { Request } from 'express'; +import { gcpIapAuthenticator } from './authenticator'; + +beforeEach(() => { + jest.clearAllMocks(); +}); +jest.mock('./helpers', () => ({ + createTokenValidator() { + return async () => ({ sub: 's', email: 'e' }); + }, +})); + +describe('GcpIapProvider', () => { + it('should find default JWT header', async () => { + const ctx = await gcpIapAuthenticator.initialize({ + config: mockServices.rootConfig({ data: { audience: 'my-audience' } }), + }); + await expect( + gcpIapAuthenticator.authenticate( + { + req: { + header(name: string) { + return name === 'x-goog-iap-jwt-assertion' + ? 'my-token' + : undefined; + }, + } as Request, + }, + ctx, + ), + ).resolves.toEqual({ result: { iapToken: { sub: 's', email: 'e' } } }); + }); + + it('should find custom JWT header', async () => { + const jwtHeader = 'x-custom-header'; + const ctx = await gcpIapAuthenticator.initialize({ + config: mockServices.rootConfig({ + data: { audience: 'my-audience', jwtHeader }, + }), + }); + await expect( + gcpIapAuthenticator.authenticate( + { + req: { + header(name: string) { + return name === jwtHeader ? 'my-token' : undefined; + }, + } as Request, + }, + ctx, + ), + ).resolves.toEqual({ result: { iapToken: { sub: 's', email: 'e' } } }); + }); + + it('should throw if header is missing', async () => { + const ctx = await gcpIapAuthenticator.initialize({ + config: mockServices.rootConfig({ + data: { audience: 'my-audience' }, + }), + }); + await expect( + gcpIapAuthenticator.authenticate( + { + req: { + header(_name: string) { + return undefined; + }, + } as Request, + }, + ctx, + ), + ).rejects.toThrow('Missing Google IAP header'); + }); +}); diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts new file mode 100644 index 0000000000..ca104ded86 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts @@ -0,0 +1,52 @@ +/* + * 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 { AuthenticationError } from '@backstage/errors'; +import { createProxyAuthenticator } from '@backstage/plugin-auth-node'; +import { createTokenValidator } from './helpers'; +import { GcpIapResult } from './types'; + +/** + * The header name used by the IAP. + */ +const DEFAULT_IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; + +/** @public */ +export const gcpIapAuthenticator = createProxyAuthenticator({ + defaultProfileTransform: async (result: GcpIapResult) => { + return { profile: { email: result.iapToken.email } }; + }, + async initialize({ config }) { + const audience = config.getString('audience'); + const jwtHeader = + config.getOptionalString('jwtHeader') ?? DEFAULT_IAP_JWT_HEADER; + + const tokenValidator = createTokenValidator(audience); + + return { jwtHeader, tokenValidator }; + }, + async authenticate({ req }, { jwtHeader, tokenValidator }) { + const token = req.header(jwtHeader); + + if (!token || typeof token !== 'string') { + throw new AuthenticationError('Missing Google IAP header'); + } + + const iapToken = await tokenValidator(token); + + return { result: { iapToken } }; + }, +}); diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts new file mode 100644 index 0000000000..4079f8f7d3 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OAuth2Client } from 'google-auth-library'; +import { createTokenValidator } from './helpers'; + +const mockJwt = 'a.b.c'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('helpers', () => { + describe('createTokenValidator', () => { + it('runs the happy path', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => ({ + getPayload: () => ({ sub: 's', email: 'e@mail.com' }), + }), + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).resolves.toMatchObject({ + sub: 's', + email: 'e@mail.com', + }); + }); + + it('throws if listing keys fail', async () => { + const mockClient = { + getIapPublicKeys: async () => { + throw new Error('NOPE'); + }, + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).rejects.toThrow( + 'Unable to list Google IAP token verification keys, Error: NOPE', + ); + }); + + it('throws if the verifying signature fails', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => { + throw new Error('NOPE'); + }, + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).rejects.toThrow( + 'Google IAP token verification failed, Error: NOPE', + ); + }); + + it('rejects empty payload', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => ({ + getPayload: () => undefined, + }), + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).rejects.toThrow( + 'Google IAP token verification failed, token had no payload', + ); + }); + + it('rejects payload without subject', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => ({ + getPayload: () => ({ email: 'e@mail.com' }), + }), + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).rejects.toThrow( + 'Google IAP token payload is missing subject claim', + ); + }); + + it('rejects payload without email', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => ({ + getPayload: () => ({ sub: 's' }), + }), + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).rejects.toThrow( + 'Google IAP token payload is missing email claim', + ); + }); + }); +}); diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts new file mode 100644 index 0000000000..54db7117f9 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AuthenticationError } from '@backstage/errors'; +import { OAuth2Client } from 'google-auth-library'; +import { GcpIapTokenInfo } from './types'; + +export function createTokenValidator( + audience: string, + providedClient?: OAuth2Client, +): (token: string) => Promise { + const client = providedClient ?? new OAuth2Client(); + + return async function tokenValidator(token) { + // TODO(freben): Rate limit the public key reads. It may be sensible to + // cache these for some reasonable time rather than asking for the public + // keys on every single sign-in. But since the rate of events here is so + // slow, I decided to keep it simple for now. + const response = await client.getIapPublicKeys().catch(error => { + throw new AuthenticationError( + `Unable to list Google IAP token verification keys, ${error}`, + ); + }); + const ticket = await client + .verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, [ + 'https://cloud.google.com/iap', + ]) + .catch(error => { + throw new AuthenticationError( + `Google IAP token verification failed, ${error}`, + ); + }); + + const payload = ticket.getPayload(); + if (!payload) { + throw new AuthenticationError( + 'Google IAP token verification failed, token had no payload', + ); + } + + if (!payload.sub) { + throw new AuthenticationError( + 'Google IAP token payload is missing subject claim', + ); + } + if (!payload.email) { + throw new AuthenticationError( + 'Google IAP token payload is missing email claim', + ); + } + + return payload as unknown as GcpIapTokenInfo; + }; +} diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/index.ts b/plugins/auth-backend-module-gcp-iap-provider/src/index.ts new file mode 100644 index 0000000000..be6626a815 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { gcpIapAuthenticator } from './authenticator'; +export { authModuleGcpIapProvider } from './module'; +export { gcpIapSignInResolvers } from './resolvers'; +export { type GcpIapResult, type GcpIapTokenInfo } from './types'; diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/module.ts b/plugins/auth-backend-module-gcp-iap-provider/src/module.ts new file mode 100644 index 0000000000..90771eecb9 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/module.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { gcpIapAuthenticator } from './authenticator'; +import { gcpIapSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleGcpIapProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'gcpIapProvider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'gcpIap', + factory: createProxyAuthProviderFactory({ + authenticator: gcpIapAuthenticator, + signInResolverFactories: { + ...gcpIapSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts new file mode 100644 index 0000000000..32ab2b6cc7 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createSignInResolverFactory, + SignInInfo, +} from '@backstage/plugin-auth-node'; +import { GcpIapResult } from './types'; + +/** + * Available sign-in resolvers for the Google auth provider. + * + * @public + */ +export namespace gcpIapSignInResolvers { + /** + * Looks up the user by matching their email to the `google.com/email` annotation. + */ + export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({ + create() { + return async (info: SignInInfo, ctx) => { + const email = info.result.iapToken.email; + + if (!email) { + throw new Error('Google IAP sign-in result is missing email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'google.com/email': email, + }, + }); + }; + }, + }); +} diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/types.ts b/plugins/auth-backend-module-gcp-iap-provider/src/types.ts new file mode 100644 index 0000000000..a967a4d579 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/types.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JsonPrimitive } from '@backstage/types'; + +/** + * The data extracted from an IAP token. + * + * @public + */ +export type GcpIapTokenInfo = { + /** + * The unique, stable identifier for the user. + */ + sub: string; + /** + * User email address. + */ + email: string; + /** + * Other fields. + */ + [key: string]: JsonPrimitive; +}; + +/** + * The result of the initial auth challenge. This is the input to the auth + * callbacks. + * + * @public + */ +export type GcpIapResult = { + /** + * The data extracted from the IAP token header. + */ + iapToken: GcpIapTokenInfo; +}; diff --git a/plugins/auth-backend-module-github-provider/.eslintrc.js b/plugins/auth-backend-module-github-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-github-provider/CHANGELOG.md b/plugins/auth-backend-module-github-provider/CHANGELOG.md new file mode 100644 index 0000000000..76bb4d7cc1 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/CHANGELOG.md @@ -0,0 +1,14 @@ +# @backstage/plugin-auth-backend-module-github-provider + +## 0.1.0-next.0 + +### Minor Changes + +- 23af27f5ce79: New module for `@backstage/plugin-auth-backend` that adds a GitHub auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 diff --git a/plugins/auth-backend-module-github-provider/README.md b/plugins/auth-backend-module-github-provider/README.md new file mode 100644 index 0000000000..7928ce1034 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/README.md @@ -0,0 +1,8 @@ +# Auth Module: GitHub Provider + +This module provides an GitHub auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Backstage](https://backstage.io) +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-github-provider) diff --git a/plugins/auth-backend-module-github-provider/api-report.md b/plugins/auth-backend-module-github-provider/api-report.md new file mode 100644 index 0000000000..2d0b33334c --- /dev/null +++ b/plugins/auth-backend-module-github-provider/api-report.md @@ -0,0 +1,29 @@ +## API Report File for "@backstage/plugin-auth-backend-module-github-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +export const authModuleGithubProvider: () => BackendFeature; + +// @public (undocumented) +export const githubAuthenticator: OAuthAuthenticator< + PassportOAuthAuthenticatorHelper, + PassportProfile +>; + +// @public +export namespace githubSignInResolvers { + const usernameMatchingUserEntityName: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; +} +``` diff --git a/plugins/auth-backend-module-github-provider/dev/index.ts b/plugins/auth-backend-module-github-provider/dev/index.ts new file mode 100644 index 0000000000..9ea084004a --- /dev/null +++ b/plugins/auth-backend-module-github-provider/dev/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackend } from '@backstage/backend-defaults'; +import { authPlugin } from '@backstage/plugin-auth-backend'; +import { authModuleGithubProvider } from '../src'; + +const backend = createBackend(); + +backend.add(authPlugin); +backend.add(authModuleGithubProvider); + +backend.start(); diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json new file mode 100644 index 0000000000..1ca6d0024e --- /dev/null +++ b/plugins/auth-backend-module-github-provider/package.json @@ -0,0 +1,41 @@ +{ + "name": "@backstage/plugin-auth-backend-module-github-provider", + "description": "The github-provider backend module for the auth plugin.", + "version": "0.1.0-next.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" + }, + "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/plugin-auth-node": "workspace:^", + "passport-github2": "^0.1.12" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "supertest": "^6.3.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/auth-backend-module-github-provider/src/authenticator.test.ts b/plugins/auth-backend-module-github-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..dc5c7208ea --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/authenticator.test.ts @@ -0,0 +1,142 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + PassportOAuthAuthenticatorHelper, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { githubAuthenticator } from './authenticator'; + +describe('githubAuthenticator', () => { + it('should store access token without expiration as refresh token', async () => { + await expect( + githubAuthenticator.authenticate( + {} as any, + { + authenticate: async _input => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + }, + }), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'access-token.my-token', + }, + }); + }); + + it('should not use access token as refresh token if it expires', async () => { + await expect( + githubAuthenticator.authenticate( + {} as any, + { + authenticate: async _input => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + expiresInSeconds: 3, + }, + }), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + expiresInSeconds: 3, + }, + }); + }); + + it('should not store access token without expiration if a refresh token is provided', async () => { + await expect( + githubAuthenticator.authenticate( + {} as any, + { + authenticate: async _input => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'my-refresh-token', + }, + }), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'my-refresh-token', + }, + }); + }); + + it('should refresh with access token', async () => { + await expect( + githubAuthenticator.refresh( + { + refreshToken: 'access-token.my-token', + req: {} as any, + scope: 'user:read', + }, + { + fetchProfile: async _input => ({ id: 'id' } as PassportProfile), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'access-token.my-token', + }, + }); + }); + + it('should refresh with refresh token', async () => { + const res = {}; + await expect( + githubAuthenticator.refresh( + { + refreshToken: 'my-refresh-token', + req: {} as any, + scope: 'user:read', + }, + { + refresh: async _input => res as any, + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toBe(res); + }); +}); diff --git a/plugins/auth-backend-module-github-provider/src/authenticator.ts b/plugins/auth-backend-module-github-provider/src/authenticator.ts new file mode 100644 index 0000000000..f59ac3d727 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/authenticator.ts @@ -0,0 +1,125 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Strategy as GithubStrategy } from 'passport-github2'; +import { + createOAuthAuthenticator, + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, +} from '@backstage/plugin-auth-node'; + +const ACCESS_TOKEN_PREFIX = 'access-token.'; + +/** @public */ +export const githubAuthenticator = createOAuthAuthenticator({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + initialize({ callbackUrl, config }) { + const clientId = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + const enterpriseInstanceUrl = config + .getOptionalString('enterpriseInstanceUrl') + ?.replace(/\/$/, ''); + const authorizationUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/authorize` + : undefined; + const tokenUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/access_token` + : undefined; + const userProfileUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/api/v3/user` + : undefined; + + return PassportOAuthAuthenticatorHelper.from( + new GithubStrategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + tokenURL: tokenUrl, + userProfileURL: userProfileUrl, + authorizationURL: authorizationUrl, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done( + undefined, + { fullProfile, params, accessToken }, + { refreshToken }, + ); + }, + ), + ); + }, + + async start(input, helper) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + + async authenticate(input, helper) { + const { fullProfile, session } = await helper.authenticate(input); + + // If we do not have a real refresh token and we have a non-expiring + // access token, then we use that as our refresh token. + if (!session.refreshToken && !session.expiresInSeconds) { + session.refreshToken = ACCESS_TOKEN_PREFIX + session.accessToken; + } + return { fullProfile, session }; + }, + + async refresh(input, helper) { + // This is the OAuth App flow. A non-expiring access token is stored in the + // refresh token cookie. We use that token to fetch the user profile and + // refresh the Backstage session when needed. + if (input.refreshToken?.startsWith(ACCESS_TOKEN_PREFIX)) { + const accessToken = input.refreshToken.slice(ACCESS_TOKEN_PREFIX.length); + + const fullProfile = await helper + .fetchProfile(accessToken) + .catch(error => { + if (error.oauthError?.statusCode === 401) { + throw new Error('Invalid access token'); + } + throw error; + }); + + return { + fullProfile, + session: { + accessToken, + tokenType: 'bearer', + scope: input.scope, + refreshToken: input.refreshToken, + // No expiration + }, + }; + } + + // This is the App flow, which is close to a standard OAuth refresh flow. It has a + // pretty long session expiration, and it also ignores the requested scope, instead + // just allowing access to whatever is configured as part of the app installation. + return helper.refresh(input); + }, +}); diff --git a/plugins/auth-backend-module-github-provider/src/index.ts b/plugins/auth-backend-module-github-provider/src/index.ts new file mode 100644 index 0000000000..ba389cbdc7 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The github-provider backend module for the auth plugin. + * + * @packageDocumentation + */ + +export { githubAuthenticator } from './authenticator'; +export { authModuleGithubProvider } from './module'; +export { githubSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-github-provider/src/module.test.ts b/plugins/auth-backend-module-github-provider/src/module.test.ts new file mode 100644 index 0000000000..d4fc36cbc6 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/module.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authPlugin } from '@backstage/plugin-auth-backend'; +import { authModuleGithubProvider } from './module'; +import request from 'supertest'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; + +describe('authModuleGithubProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleGithubProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + github: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get('/api/auth/github/start?env=development'); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('github-nonce', { + domain: 'localhost', + path: '/api/auth/github/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://github.com'); + expect(startUrl.pathname).toBe('/login/oauth/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + client_id: 'my-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/github/handler/frame`, + state: expect.any(String), + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); +}); diff --git a/plugins/auth-backend-module-github-provider/src/module.ts b/plugins/auth-backend-module-github-provider/src/module.ts new file mode 100644 index 0000000000..54286c94a7 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/module.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { githubAuthenticator } from './authenticator'; +import { githubSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleGithubProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'github-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'github', + factory: createOAuthProviderFactory({ + authenticator: githubAuthenticator, + signInResolverFactories: { + ...githubSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-github-provider/src/resolvers.ts b/plugins/auth-backend-module-github-provider/src/resolvers.ts new file mode 100644 index 0000000000..496080a33c --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/resolvers.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +/** + * Available sign-in resolvers for the GitHub auth provider. + * + * @public + */ +export namespace githubSignInResolvers { + /** + * Looks up the user by matching their GitHub username to the entity name. + */ + export const usernameMatchingUserEntityName = createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { fullProfile } = info.result; + + const userId = fullProfile.username; + if (!userId) { + throw new Error(`GitHub user profile does not contain a username`); + } + + return ctx.signInWithCatalogUser({ entityRef: { name: userId } }); + }; + }, + }); +} diff --git a/plugins/auth-backend-module-gitlab-provider/.eslintrc.js b/plugins/auth-backend-module-gitlab-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-gitlab-provider/README.md b/plugins/auth-backend-module-gitlab-provider/README.md new file mode 100644 index 0000000000..b9eb561990 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/README.md @@ -0,0 +1,8 @@ +# Auth Module: GitLab Provider + +This module provides an GitLab auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Repository](https://gitlab.com/backstage/backstage/tree/master/plugins/auth-backend-module-gitlab-provider) +- [Backstage Project Homepage](https://backstage.io) diff --git a/plugins/auth-backend-module-gitlab-provider/api-report.md b/plugins/auth-backend-module-gitlab-provider/api-report.md new file mode 100644 index 0000000000..e93208688a --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/api-report.md @@ -0,0 +1,29 @@ +## API Report File for "@backstage/plugin-auth-backend-module-gitlab-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +export const authModuleGitlabProvider: () => BackendFeature; + +// @public (undocumented) +export const gitlabAuthenticator: OAuthAuthenticator< + PassportOAuthAuthenticatorHelper, + PassportProfile +>; + +// @public +export namespace gitlabSignInResolvers { + const usernameMatchingUserEntityName: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; +} +``` diff --git a/plugins/auth-backend-module-gitlab-provider/config.d.ts b/plugins/auth-backend-module-gitlab-provider/config.d.ts new file mode 100644 index 0000000000..d7f04d872a --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/config.d.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + auth?: { + providers?: { + /** @visibility frontend */ + gitlab?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + audience?: string; + callbackUrl?: string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-gitlab-provider/dev/index.ts b/plugins/auth-backend-module-gitlab-provider/dev/index.ts new file mode 100644 index 0000000000..b2b92e5cb5 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/dev/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackend } from '@backstage/backend-defaults'; +import { authPlugin } from '@backstage/plugin-auth-backend'; +import { authModuleGitlabProvider } from '../src'; + +const backend = createBackend(); + +backend.add(authPlugin); +backend.add(authModuleGitlabProvider); + +backend.start(); diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json new file mode 100644 index 0000000000..5fd3f4e0d7 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -0,0 +1,45 @@ +{ + "name": "@backstage/plugin-auth-backend-module-gitlab-provider", + "description": "The gitlab-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" + }, + "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/plugin-auth-node": "workspace:^", + "express": "^4.18.2", + "passport": "^0.6.0", + "passport-gitlab2": "^5.0.0" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "supertest": "^6.3.3" + }, + "configSchema": "config.d.ts", + "files": [ + "dist", + "config.d.ts" + ] +} diff --git a/plugins/auth-backend-module-gitlab-provider/src/authenticator.ts b/plugins/auth-backend-module-gitlab-provider/src/authenticator.ts new file mode 100644 index 0000000000..9cd5857b0d --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/src/authenticator.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Strategy as GitlabStrategy } from 'passport-gitlab2'; +import { + createOAuthAuthenticator, + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, +} from '@backstage/plugin-auth-node'; + +/** @public */ +export const gitlabAuthenticator = createOAuthAuthenticator({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + initialize({ callbackUrl, config }) { + const clientId = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + const baseUrl = + config.getOptionalString('audience') || 'https://gitlab.com'; + + return PassportOAuthAuthenticatorHelper.from( + new GitlabStrategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + baseURL: baseUrl, + authorizationURL: `${baseUrl}/oauth/authorize`, + tokenURL: `${baseUrl}/oauth/token`, + profileURL: `${baseUrl}/api/v4/user`, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done( + undefined, + { fullProfile, params, accessToken }, + { refreshToken }, + ); + }, + ), + ); + }, + + async start(input, helper) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + + async authenticate(input, helper) { + return helper.authenticate(input); + }, + + async refresh(input, helper) { + return helper.refresh(input); + }, +}); diff --git a/plugins/auth-backend-module-gitlab-provider/src/index.ts b/plugins/auth-backend-module-gitlab-provider/src/index.ts new file mode 100644 index 0000000000..7136b89612 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The gitlab-provider backend module for the auth plugin. + * + * @packageDocumentation + */ + +export { gitlabAuthenticator } from './authenticator'; +export { authModuleGitlabProvider } from './module'; +export { gitlabSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-gitlab-provider/src/module.test.ts b/plugins/auth-backend-module-gitlab-provider/src/module.test.ts new file mode 100644 index 0000000000..633a8ee639 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/src/module.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authPlugin } from '@backstage/plugin-auth-backend'; +import { authModuleGitlabProvider } from './module'; +import request from 'supertest'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; + +describe('authModuleGitlabProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleGitlabProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + gitlab: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get('/api/auth/gitlab/start?env=development'); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('gitlab-nonce', { + domain: 'localhost', + path: '/api/auth/gitlab/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://gitlab.com'); + expect(startUrl.pathname).toBe('/oauth/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + scope: 'read_user', + client_id: 'my-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/gitlab/handler/frame`, + state: expect.any(String), + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); +}); diff --git a/plugins/auth-backend-module-gitlab-provider/src/module.ts b/plugins/auth-backend-module-gitlab-provider/src/module.ts new file mode 100644 index 0000000000..790d522494 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/src/module.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { gitlabAuthenticator } from './authenticator'; +import { gitlabSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleGitlabProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'gitlab-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'gitlab', + factory: createOAuthProviderFactory({ + authenticator: gitlabAuthenticator, + signInResolverFactories: { + ...gitlabSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts new file mode 100644 index 0000000000..755ed08aa0 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +/** + * Available sign-in resolvers for the GitLab auth provider. + * + * @public + */ +export namespace gitlabSignInResolvers { + /** + * Looks up the user by matching their GitLab username to the entity name. + */ + export const usernameMatchingUserEntityName = createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { result } = info; + + const id = result.fullProfile.username; + if (!id) { + throw new Error(`GitLab user profile does not contain a username`); + } + + return ctx.signInWithCatalogUser({ entityRef: { name: id } }); + }; + }, + }); +} diff --git a/plugins/auth-backend/src/providers/gitlab/types.d.ts b/plugins/auth-backend-module-gitlab-provider/src/types.d.ts similarity index 100% rename from plugins/auth-backend/src/providers/gitlab/types.d.ts rename to plugins/auth-backend-module-gitlab-provider/src/types.d.ts diff --git a/plugins/auth-backend-module-google-provider/.eslintrc.js b/plugins/auth-backend-module-google-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-google-provider/CHANGELOG.md b/plugins/auth-backend-module-google-provider/CHANGELOG.md new file mode 100644 index 0000000000..8dc2f1d632 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-auth-backend-module-google-provider + +## 0.1.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: New module for `@backstage/plugin-auth-backend` that adds a Google auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 diff --git a/plugins/auth-backend-module-google-provider/README.md b/plugins/auth-backend-module-google-provider/README.md new file mode 100644 index 0000000000..633dd13717 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/README.md @@ -0,0 +1,8 @@ +# Auth Module: Google Provider + +This module provides a Google auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Backstage](https://backstage.io) +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-google-provider) diff --git a/plugins/auth-backend-module-google-provider/api-report.md b/plugins/auth-backend-module-google-provider/api-report.md new file mode 100644 index 0000000000..8040c44832 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/api-report.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/plugin-auth-backend-module-google-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +export const authModuleGoogleProvider: () => BackendFeature; + +// @public (undocumented) +export const googleAuthenticator: OAuthAuthenticator< + PassportOAuthAuthenticatorHelper, + PassportProfile +>; + +// @public +export namespace googleSignInResolvers { + const emailMatchingUserEntityAnnotation: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/auth-backend-module-google-provider/config.d.ts b/plugins/auth-backend-module-google-provider/config.d.ts new file mode 100644 index 0000000000..6a1716c5bf --- /dev/null +++ b/plugins/auth-backend-module-google-provider/config.d.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + /** Configuration options for the auth plugin */ + auth?: { + providers?: { + google?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + callbackUrl?: string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json new file mode 100644 index 0000000000..c78889c312 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-auth-backend-module-google-provider", + "description": "A Google auth provider module for the Backstage auth backend", + "version": "0.1.0-next.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" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-google-provider" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "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" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "google-auth-library": "^8.0.0", + "passport-google-oauth20": "^2.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "@types/passport-google-oauth20": "^2.0.3", + "msw": "^1.0.0", + "supertest": "^6.1.3" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/auth-backend-module-google-provider/src/authenticator.ts b/plugins/auth-backend-module-google-provider/src/authenticator.ts new file mode 100644 index 0000000000..a428519fee --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/authenticator.ts @@ -0,0 +1,86 @@ +/* + * 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 { + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, + createOAuthAuthenticator, +} from '@backstage/plugin-auth-node'; +import { OAuth2Client } from 'google-auth-library'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; + +/** @public */ +export const googleAuthenticator = createOAuthAuthenticator({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + initialize({ callbackUrl, config }) { + const clientId = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + + return PassportOAuthAuthenticatorHelper.from( + new GoogleStrategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + passReqToCallback: false, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done( + undefined, + { + fullProfile, + params, + accessToken, + }, + { + refreshToken, + }, + ); + }, + ), + ); + }, + + async start(input, helper) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + + async authenticate(input, helper) { + return helper.authenticate(input); + }, + + async refresh(input, helper) { + return helper.refresh(input); + }, + + async logout(input) { + if (input.refreshToken) { + const oauthClient = new OAuth2Client(); + await oauthClient.revokeToken(input.refreshToken); + } + }, +}); diff --git a/plugins/auth-backend-module-google-provider/src/index.ts b/plugins/auth-backend-module-google-provider/src/index.ts new file mode 100644 index 0000000000..9999dd2bc5 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { googleAuthenticator } from './authenticator'; +export { authModuleGoogleProvider } from './module'; +export { googleSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-google-provider/src/module.test.ts b/plugins/auth-backend-module-google-provider/src/module.test.ts new file mode 100644 index 0000000000..d186064230 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/module.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authPlugin } from '@backstage/plugin-auth-backend'; +import { authModuleGoogleProvider } from './module'; +import request from 'supertest'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; + +describe('authModuleGoogleProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleGoogleProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + google: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get('/api/auth/google/start?env=development'); + + expect(res.status).toBe(302); + + const nonceCookie = agent.jar.getCookie('google-nonce', { + domain: 'localhost', + path: '/api/auth/google/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://accounts.google.com'); + expect(startUrl.pathname).toBe('/o/oauth2/v2/auth'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + access_type: 'offline', + prompt: 'consent', + response_type: 'code', + client_id: 'my-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/google/handler/frame`, + state: expect.any(String), + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); +}); diff --git a/plugins/auth-backend-module-google-provider/src/module.ts b/plugins/auth-backend-module-google-provider/src/module.ts new file mode 100644 index 0000000000..0354f99880 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/module.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { googleAuthenticator } from './authenticator'; +import { googleSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleGoogleProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'googleProvider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'google', + factory: createOAuthProviderFactory({ + authenticator: googleAuthenticator, + signInResolverFactories: { + ...googleSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-google-provider/src/resolvers.ts b/plugins/auth-backend-module-google-provider/src/resolvers.ts new file mode 100644 index 0000000000..b19fc4d49f --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/resolvers.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +/** + * Available sign-in resolvers for the Google auth provider. + * + * @public + */ +export namespace googleSignInResolvers { + /** + * Looks up the user by matching their email to the `google.com/email` annotation. + */ + export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Google profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'google.com/email': profile.email, + }, + }); + }; + }, + }); +} diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 5504f162e6..4250d917ad 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-auth-backend +## 0.18.8-next.0 + +### Patch Changes + +- 7944d43f4790: Added `authPlugin` export for the new backend system. The plugin does not include any built-in auth providers, they must instead be added by installing additional modules, for example `authModuleGoogleProvider` from `@backstage/plugin-auth-backend-module-google-provider`. +- 8513cd7d00e3: Deprecated several exports that are now available from `@backstage/plugin-auth-node` instead. +- 7944d43f4790: Added the ability to disable the built-in auth providers by passing `disableDefaultProviderFactories` to `createRouter`. +- 7944d43f4790: The algorithm used when generating Backstage tokens can be configured via `auth.identityTokenAlgorithm`. +- Updated dependencies + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + ## 0.18.6 ### Patch Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 80db2e0b71..d50a26db2e 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -5,98 +5,73 @@ ```ts /// -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { AuthProviderConfig as AuthProviderConfig_2 } from '@backstage/plugin-auth-node'; +import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin-auth-node'; +import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node'; +import { AuthResolverCatalogUserQuery as AuthResolverCatalogUserQuery_2 } from '@backstage/plugin-auth-node'; +import { AuthResolverContext as AuthResolverContext_2 } from '@backstage/plugin-auth-node'; +import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageSignInResult } from '@backstage/plugin-auth-node'; import { CacheService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; +import { ClientAuthResponse } from '@backstage/plugin-auth-node'; import { Config } from '@backstage/config'; +import { CookieConfigurer as CookieConfigurer_2 } from '@backstage/plugin-auth-node'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; +import { encodeOAuthState } from '@backstage/plugin-auth-node'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; -import { GetEntitiesRequest } from '@backstage/catalog-client'; +import { GcpIapResult as GcpIapResult_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider'; +import { GcpIapTokenInfo as GcpIapTokenInfo_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider'; import { IncomingHttpHeaders } from 'http'; -import { JsonValue } from '@backstage/types'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { OAuthEnvironmentHandler as OAuthEnvironmentHandler_2 } from '@backstage/plugin-auth-node'; +import { OAuthState as OAuthState_2 } from '@backstage/plugin-auth-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { prepareBackstageIdentityResponse as prepareBackstageIdentityResponse_2 } from '@backstage/plugin-auth-node'; import { Profile } from 'passport'; +import { ProfileInfo as ProfileInfo_2 } from '@backstage/plugin-auth-node'; +import { SignInInfo as SignInInfo_2 } from '@backstage/plugin-auth-node'; +import { SignInResolver as SignInResolver_2 } from '@backstage/plugin-auth-node'; import { TokenManager } from '@backstage/backend-common'; +import { TokenParams as TokenParams_2 } from '@backstage/plugin-auth-node'; import { TokenSet } from 'openid-client'; import { UserEntity } from '@backstage/catalog-model'; import { UserinfoResponse } from 'openid-client'; +import { WebMessageResponse as WebMessageResponse_2 } from '@backstage/plugin-auth-node'; -// @public +// @public @deprecated export type AuthHandler = ( input: TAuthResult, context: AuthResolverContext, ) => Promise; -// @public +// @public @deprecated export type AuthHandlerResult = { profile: ProfileInfo; }; -// @public (undocumented) -export type AuthProviderConfig = { - baseUrl: string; - appUrl: string; - isOriginAllowed: (origin: string) => boolean; - cookieConfigurer?: CookieConfigurer; -}; - -// @public (undocumented) -export type AuthProviderFactory = (options: { - providerId: string; - globalConfig: AuthProviderConfig; - config: Config; - logger: Logger; - resolverContext: AuthResolverContext; -}) => AuthProviderRouteHandlers; - // @public -export interface AuthProviderRouteHandlers { - frameHandler(req: express.Request, res: express.Response): Promise; - logout?(req: express.Request, res: express.Response): Promise; - refresh?(req: express.Request, res: express.Response): Promise; - start(req: express.Request, res: express.Response): Promise; -} +export const authPlugin: () => BackendFeature; -// @public -export type AuthResolverCatalogUserQuery = - | { - entityRef: - | string - | { - kind?: string; - namespace?: string; - name: string; - }; - } - | { - annotations: Record; - } - | { - filter: Exclude; - }; +// @public @deprecated (undocumented) +export type AuthProviderConfig = AuthProviderConfig_2; -// @public -export type AuthResolverContext = { - issueToken(params: TokenParams): Promise<{ - token: string; - }>; - findCatalogUser(query: AuthResolverCatalogUserQuery): Promise<{ - entity: Entity; - }>; - signInWithCatalogUser( - query: AuthResolverCatalogUserQuery, - ): Promise; -}; +// @public @deprecated (undocumented) +export type AuthProviderFactory = AuthProviderFactory_2; -// @public (undocumented) -export type AuthResponse = { - providerInfo: ProviderInfo; - profile: ProfileInfo; - backstageIdentity?: BackstageIdentityResponse; -}; +// @public @deprecated (undocumented) +export type AuthProviderRouteHandlers = AuthProviderRouteHandlers_2; + +// @public @deprecated (undocumented) +export type AuthResolverCatalogUserQuery = AuthResolverCatalogUserQuery_2; + +// @public @deprecated (undocumented) +export type AuthResolverContext = AuthResolverContext_2; + +// @public @deprecated (undocumented) +export type AuthResponse = ClientAuthResponse; // @public (undocumented) export type AwsAlbResult = { @@ -151,7 +126,7 @@ export class CatalogIdentityClient { findUser(query: { annotations: Record }): Promise; resolveCatalogMembership(query: { entityRefs: string[]; - logger?: Logger; + logger?: LoggerService; }): Promise; } @@ -191,18 +166,8 @@ export type CloudflareAccessResult = { token: string; }; -// @public -export type CookieConfigurer = (ctx: { - providerId: string; - baseUrl: string; - callbackUrl: string; - appOrigin: string; -}) => { - domain: string; - path: string; - secure: boolean; - sameSite?: 'none' | 'lax' | 'strict'; -}; +// @public @deprecated (undocumented) +export type CookieConfigurer = CookieConfigurer_2; // @public export function createAuthProviderIntegration< @@ -235,23 +200,17 @@ export type EasyAuthResult = { accessToken?: string; }; -// @public (undocumented) -export const encodeState: (state: OAuthState) => string; +// @public @deprecated (undocumented) +export const encodeState: typeof encodeOAuthState; -// @public (undocumented) +// @public @deprecated (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; -// @public -export type GcpIapResult = { - iapToken: GcpIapTokenInfo; -}; +// @public @deprecated +export type GcpIapResult = GcpIapResult_2; -// @public -export type GcpIapTokenInfo = { - sub: string; - email: string; - [key: string]: JsonValue; -}; +// @public @deprecated +export type GcpIapTokenInfo = GcpIapTokenInfo_2; // @public export function getDefaultOwnershipEntityRefs(entity: Entity): string[]; @@ -276,7 +235,7 @@ export type OAuth2ProxyResult = { getHeader(name: string): string | undefined; }; -// @public (undocumented) +// @public @deprecated (undocumented) export class OAuthAdapter implements AuthProviderRouteHandlers { constructor(handlers: OAuthHandlers, options: OAuthAdapterOptions); // (undocumented) @@ -298,7 +257,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { start(req: express.Request, res: express.Response): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthAdapterOptions = { providerId: string; persistScopes?: boolean; @@ -309,25 +268,10 @@ export type OAuthAdapterOptions = { callbackUrl: string; }; -// @public (undocumented) -export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { - constructor(handlers: Map); - // (undocumented) - frameHandler(req: express.Request, res: express.Response): Promise; - // (undocumented) - logout(req: express.Request, res: express.Response): Promise; - // (undocumented) - static mapConfig( - config: Config, - factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, - ): OAuthEnvironmentHandler; - // (undocumented) - refresh(req: express.Request, res: express.Response): Promise; - // (undocumented) - start(req: express.Request, res: express.Response): Promise; -} +// @public @deprecated (undocumented) +export const OAuthEnvironmentHandler: typeof OAuthEnvironmentHandler_2; -// @public +// @public @deprecated (undocumented) export interface OAuthHandlers { handler(req: express.Request): Promise<{ response: OAuthResponse; @@ -341,12 +285,12 @@ export interface OAuthHandlers { start(req: OAuthStartRequest): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthLogoutRequest = express.Request<{}> & { refreshToken: string; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthProviderInfo = { accessToken: string; idToken?: string; @@ -354,59 +298,53 @@ export type OAuthProviderInfo = { scope: string; }; -// @public +// @public @deprecated export type OAuthProviderOptions = { clientId: string; clientSecret: string; callbackUrl: string; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthRefreshRequest = express.Request<{}> & { scope: string; refreshToken: string; }; -// @public +// @public @deprecated (undocumented) export type OAuthResponse = { profile: ProfileInfo; providerInfo: OAuthProviderInfo; backstageIdentity?: BackstageSignInResult; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthResult = { fullProfile: Profile; params: { id_token?: string; scope: string; + token_type?: string; expires_in: number; }; accessToken: string; refreshToken?: string; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthStartRequest = express.Request<{}> & { scope: string; state: OAuthState; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthStartResponse = { url: string; status?: number; }; -// @public (undocumented) -export type OAuthState = { - nonce: string; - env: string; - origin?: string; - scope?: string; - redirectUrl?: string; - flow?: string; -}; +// @public @deprecated (undocumented) +export type OAuthState = OAuthState_2; // @public export type OidcAuthResult = { @@ -414,24 +352,18 @@ export type OidcAuthResult = { userinfo: UserinfoResponse; }; -// @public (undocumented) +// @public @deprecated (undocumented) export const postMessageResponse: ( res: express.Response, appOrigin: string, response: WebMessageResponse, ) => void; -// @public -export function prepareBackstageIdentityResponse( - result: BackstageSignInResult, -): BackstageIdentityResponse; +// @public @deprecated (undocumented) +export const prepareBackstageIdentityResponse: typeof prepareBackstageIdentityResponse_2; -// @public -export type ProfileInfo = { - email?: string; - displayName?: string; - picture?: string; -}; +// @public @deprecated (undocumented) +export type ProfileInfo = ProfileInfo_2; // @public (undocumented) export type ProviderFactories = { @@ -452,7 +384,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; auth0: Readonly<{ @@ -467,7 +399,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; awsAlb: Readonly<{ @@ -480,7 +412,7 @@ export const providers: Readonly<{ }; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; bitbucket: Readonly<{ @@ -495,7 +427,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ usernameMatchingUserEntityAnnotation(): SignInResolver; userIdMatchingUserEntityAnnotation(): SignInResolver; @@ -513,7 +445,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ emailMatchingUserEntityProfileEmail: () => SignInResolver; }>; @@ -525,18 +457,18 @@ export const providers: Readonly<{ resolver: SignInResolver; }; cache?: CacheService | undefined; - }) => AuthProviderFactory; + }) => AuthProviderFactory_2; resolvers: Readonly<{ emailMatchingUserEntityProfileEmail: () => SignInResolver; }>; }>; gcpIap: Readonly<{ create: (options: { - authHandler?: AuthHandler | undefined; + authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver; }; - }) => AuthProviderFactory; + }) => AuthProviderFactory_2; resolvers: never; }>; github: Readonly<{ @@ -546,15 +478,15 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; stateEncoder?: StateEncoder | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ - usernameMatchingUserEntityName: () => SignInResolver; + usernameMatchingUserEntityName: () => SignInResolver_2; }>; }>; gitlab: Readonly<{ @@ -569,7 +501,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; google: Readonly<{ @@ -584,11 +516,11 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ - emailLocalPartMatchingUserEntityName: () => SignInResolver; - emailMatchingUserEntityProfileEmail: () => SignInResolver; - emailMatchingUserEntityAnnotation(): SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver_2; + emailLocalPartMatchingUserEntityName: () => SignInResolver_2; + emailMatchingUserEntityAnnotation: () => SignInResolver_2; }>; }>; microsoft: Readonly<{ @@ -603,7 +535,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ emailLocalPartMatchingUserEntityName: () => SignInResolver; emailMatchingUserEntityProfileEmail: () => SignInResolver; @@ -622,7 +554,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; oauth2Proxy: Readonly<{ @@ -631,7 +563,7 @@ export const providers: Readonly<{ signIn: { resolver: SignInResolver>; }; - }) => AuthProviderFactory; + }) => AuthProviderFactory_2; resolvers: never; }>; oidc: Readonly<{ @@ -646,7 +578,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ emailLocalPartMatchingUserEntityName: () => SignInResolver; emailMatchingUserEntityProfileEmail: () => SignInResolver; @@ -664,7 +596,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ emailLocalPartMatchingUserEntityName: () => SignInResolver; emailMatchingUserEntityProfileEmail: () => SignInResolver; @@ -683,7 +615,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; saml: Readonly<{ @@ -698,7 +630,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ nameIdMatchingUserEntityName(): SignInResolver; }>; @@ -713,13 +645,13 @@ export const providers: Readonly<{ }; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; }>; -// @public (undocumented) -export const readState: (stateString: string) => OAuthState; +// @public @deprecated (undocumented) +export const readState: typeof decodeOAuthState; // @public (undocumented) export interface RouterOptions { @@ -730,9 +662,11 @@ export interface RouterOptions { // (undocumented) database: PluginDatabaseManager; // (undocumented) + disableDefaultProviderFactories?: boolean; + // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) - logger: Logger; + logger: LoggerService; // (undocumented) providerFactories?: ProviderFactories; // (undocumented) @@ -746,42 +680,23 @@ export type SamlAuthResult = { fullProfile: any; }; -// @public -export type SignInInfo = { - profile: ProfileInfo; - result: TAuthResult; -}; +// @public @deprecated (undocumented) +export type SignInInfo = SignInInfo_2; -// @public -export type SignInResolver = ( - info: SignInInfo, - context: AuthResolverContext, -) => Promise; +// @public @deprecated (undocumented) +export type SignInResolver = SignInResolver_2; -// @public (undocumented) +// @public @deprecated (undocumented) export type StateEncoder = (req: OAuthStartRequest) => Promise<{ encodedState: string; }>; -// @public -export type TokenParams = { - claims: { - sub: string; - ent?: string[]; - } & Record; -}; +// @public @deprecated (undocumented) +export type TokenParams = TokenParams_2; -// @public (undocumented) +// @public @deprecated (undocumented) export const verifyNonce: (req: express.Request, providerId: string) => void; -// @public -export type WebMessageResponse = - | { - type: 'authorization_response'; - response: AuthResponse; - } - | { - type: 'authorization_response'; - error: Error; - }; +// @public @deprecated (undocumented) +export type WebMessageResponse = WebMessageResponse_2; ``` diff --git a/plugins/auth-backend/architecture.drawio.svg b/plugins/auth-backend/architecture.drawio.svg new file mode 100644 index 0000000000..d709935e3e --- /dev/null +++ b/plugins/auth-backend/architecture.drawio.svg @@ -0,0 +1,262 @@ + + + + + + + +
+
+
+ PassportStrategy +
+
+
+
+ + PassportStrategy + +
+
+ + + + + + + + +
+
+
+ OAuthAuthenticator +
+
+
+
+ + OAuthAuthenticator + +
+
+ + + + + + + + + + + + +
+
+
+ ProxyProviderFactory +
+
+
+
+ + ProxyProviderFactory + +
+
+ + + + + + + + + + + + + + + + +
+
+
+ OAuthProviderFactory +
+
+
+
+ + OAuthProviderFactory + +
+
+ + + + +
+
+
+ ProfileTransform +
+
+
+
+ + ProfileTransform + +
+
+ + + + +
+
+
+ ProxyAuthenticator +
+
+
+
+ + ProxyAuthenticator + +
+
+ + + + +
+
+
+ AccessCheck +
+
+
+
+ + AccessCheck + +
+
+ + + + +
+
+
+ SignInResolver +
+
+
+
+ + SignInResolver + +
+
+ + + + + + +
+
+
+ authModule*Provider +
+
+
+
+ + authModule*Provider + +
+
+ + + + +
+
+
+ OAuthEnvironmentHandler +
+
+
+
+ + OAuthEnvironmentHandler + +
+
+ + + + +
+
+
+ OAuthAdapter +
+
+
+
+ + OAuthAdapter + +
+
+ + + + +
+
+
+ PassportOAuthAuthenticatorHelper +
+
+
+
+ + PassportOAuthAuthenticatorHelper + +
+
+ + + + + + +
+
+
+ authModule*Provider +
+
+
+
+ + authModule*Provider + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 4ebf7c9de1..e719468ac1 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -31,6 +31,16 @@ export interface Config { secret?: string; }; + /** + * JWS "alg" (Algorithm) Header Parameter value. Defaults to ES256. + * Must match one of the algorithms defined for IdentityClient. + * When setting a different algorithm, check if the `key` field + * of the `signing_keys` table can fit the length of the generated keys. + * If not, add a knex migration file in the migrations folder. + * More info on supported algorithms: https://github.com/panva/jose + */ + identityTokenAlgorithm?: string; + /** To control how to store JWK data in auth-backend */ keyStore?: { provider?: 'database' | 'memory' | 'firestore'; @@ -86,18 +96,6 @@ export interface Config { }; }; /** @visibility frontend */ - gitlab?: { - [authEnv: string]: { - clientId: string; - /** - * @visibility secret - */ - clientSecret: string; - audience?: string; - callbackUrl?: string; - }; - }; - /** @visibility frontend */ saml?: { entryPoint: string; logoutUrl?: string; diff --git a/plugins/auth-backend/dev/index.ts b/plugins/auth-backend/dev/index.ts new file mode 100644 index 0000000000..4d295bdc77 --- /dev/null +++ b/plugins/auth-backend/dev/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackend } from '@backstage/backend-defaults'; +import { authPlugin } from '../src'; +import { authModuleGoogleProvider } from '@backstage/plugin-auth-backend-module-google-provider'; + +const backend = createBackend(); + +backend.add(authPlugin); +backend.add(authModuleGoogleProvider); + +backend.start(); diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index be2017e4db..5cedf974a4 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.18.6", + "version": "0.18.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,11 +33,17 @@ }, "dependencies": { "@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-backend-module-gcp-iap-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-google-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", "@davidzemon/passport-okta-oauth": "^0.0.5", "@google-cloud/firestore": "^6.0.0", @@ -77,6 +83,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/body-parser": "^1.19.0", diff --git a/plugins/auth-backend/src/authPlugin.test.ts b/plugins/auth-backend/src/authPlugin.test.ts new file mode 100644 index 0000000000..d8a15eba3e --- /dev/null +++ b/plugins/auth-backend/src/authPlugin.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import request from 'supertest'; +import { authPlugin } from './authPlugin'; + +describe('authPlugin', () => { + it('should provide an OpenID configuration', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + }, + }), + ], + }); + + const res = await request(server).get( + '/api/auth/.well-known/openid-configuration', + ); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ + claims_supported: ['sub'], + issuer: `http://localhost:${server.port()}/api/auth`, + }); + }); +}); diff --git a/plugins/auth-backend/src/authPlugin.ts b/plugins/auth-backend/src/authPlugin.ts new file mode 100644 index 0000000000..0522424a99 --- /dev/null +++ b/plugins/auth-backend/src/authPlugin.ts @@ -0,0 +1,82 @@ +/* + * 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 { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { + AuthProviderFactory, + authProvidersExtensionPoint, +} from '@backstage/plugin-auth-node'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { createRouter } from './service/router'; + +/** + * Auth plugin + * + * @public + */ +export const authPlugin = createBackendPlugin({ + pluginId: 'auth', + register(reg) { + const providers = new Map(); + + reg.registerExtensionPoint(authProvidersExtensionPoint, { + registerProvider({ providerId, factory }) { + if (providers.has(providerId)) { + throw new Error( + `Auth provider '${providerId}' was already registered`, + ); + } + providers.set(providerId, factory); + }, + }); + + reg.registerInit({ + deps: { + httpRouter: coreServices.httpRouter, + logger: coreServices.logger, + config: coreServices.rootConfig, + database: coreServices.database, + discovery: coreServices.discovery, + tokenManager: coreServices.tokenManager, + catalogApi: catalogServiceRef, + }, + async init({ + httpRouter, + logger, + config, + database, + discovery, + tokenManager, + catalogApi, + }) { + const router = await createRouter({ + logger, + config, + database, + discovery, + tokenManager, + catalogApi, + providerFactories: Object.fromEntries(providers), + disableDefaultProviderFactories: true, + }); + httpRouter.use(router); + }, + }); + }, +}); diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts index a5479a6470..b5f4f5dcfb 100644 --- a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { DocumentData, Firestore, @@ -57,7 +57,7 @@ export class FirestoreKeyStore implements KeyStore { static async verifyConnection( keyStore: FirestoreKeyStore, - logger?: Logger, + logger?: LoggerService, ): Promise { try { await keyStore.verify(); diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts index 4cd8f91842..cc35c0d84e 100644 --- a/plugins/auth-backend/src/identity/KeyStores.ts +++ b/plugins/auth-backend/src/identity/KeyStores.ts @@ -15,7 +15,7 @@ */ import { pickBy } from 'lodash'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; @@ -26,7 +26,7 @@ import { MemoryKeyStore } from './MemoryKeyStore'; import { KeyStore } from './types'; type Options = { - logger: Logger; + logger: LoggerService; database: AuthDatabase; }; diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index f80b79b6bc..ca16f8821f 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -18,14 +18,14 @@ import { AuthenticationError } from '@backstage/errors'; import { exportJWK, generateKeyPair, importJWK, JWK, SignJWT } from 'jose'; import { DateTime } from 'luxon'; import { v4 as uuid } from 'uuid'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { AnyJWK, KeyStore, TokenIssuer, TokenParams } from './types'; const MS_IN_S = 1000; type Options = { - logger: Logger; + logger: LoggerService; /** Value of the issuer claim in issued tokens */ issuer: string; /** Key store used for storing signing keys */ @@ -57,7 +57,7 @@ type Options = { */ export class TokenFactory implements TokenIssuer { private readonly issuer: string; - private readonly logger: Logger; + private readonly logger: LoggerService; private readonly keyStore: KeyStore; private readonly keyDurationSeconds: number; private readonly algorithm: string; diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index e325f74c81..fcfc0345cc 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/types'; +import { TokenParams as _TokenParams } from '@backstage/plugin-auth-node'; /** Represents any form of serializable JWK */ export interface AnyJWK extends Record { @@ -25,26 +25,10 @@ export interface AnyJWK extends Record { } /** - * Parameters used to issue new ID Tokens - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type TokenParams = { - /** - * The claims that will be embedded within the token. At a minimum, this should include - * the subject claim, `sub`. It is common to also list entity ownership relations in the - * `ent` list. Additional claims may also be added at the developer's discretion except - * for the following list, which will be overwritten by the TokenIssuer: `iss`, `aud`, - * `iat`, and `exp`. The Backstage team also maintains the right add new claims in the future - * without listing the change as a "breaking change". - */ - claims: { - /** The token subject, i.e. User ID */ - sub: string; - /** A list of entity references that the user claims ownership through */ - ent?: string[]; - } & Record; -}; +export type TokenParams = _TokenParams; /** * A TokenIssuer is able to issue verifiable ID Tokens on demand. diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index f8a8580359..d8323e02ee 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export { authPlugin } from './authPlugin'; export * from './service/router'; export type { TokenParams } from './identity'; export * from './providers'; diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 494e8b4d38..ff983e27a5 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { CatalogApi } from '@backstage/catalog-client'; import { @@ -78,7 +78,7 @@ export class CatalogIdentityClient { */ async resolveCatalogMembership(query: { entityRefs: string[]; - logger?: Logger; + logger?: LoggerService; }): Promise { const { entityRefs, logger } = query; const resolvedEntityRefs = entityRefs diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts index 94ac4fba15..2f1770a3d4 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts @@ -24,7 +24,10 @@ export const safelyEncodeURIComponent = (value: string) => { return encodeURIComponent(value).replace(/'/g, '%27'); }; -/** @public */ +/** + * @public + * @deprecated Use `sendWebMessageResponse` from `@backstage/plugin-auth-node` instead + */ export const postMessageResponse = ( res: express.Response, appOrigin: string, @@ -69,7 +72,10 @@ export const postMessageResponse = ( res.end(``); }; -/** @public */ +/** + * @public + * @deprecated Use inline logic to check that the `X-Requested-With` header is set to `'XMLHttpRequest'` instead. + */ export const ensuresXRequestedWith = (req: express.Request) => { const requiredHeader = req.header('X-Requested-With'); if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { diff --git a/plugins/auth-backend/src/lib/flow/types.ts b/plugins/auth-backend/src/lib/flow/types.ts index f67198a1b0..36f3033b21 100644 --- a/plugins/auth-backend/src/lib/flow/types.ts +++ b/plugins/auth-backend/src/lib/flow/types.ts @@ -14,20 +14,10 @@ * limitations under the License. */ -import { AuthResponse } from '../../providers/types'; +import { WebMessageResponse as _WebMessageResponse } from '@backstage/plugin-auth-node'; /** - * Payload sent as a post message after the auth request is complete. - * If successful then has a valid payload with Auth information else contains an error. - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type WebMessageResponse = - | { - type: 'authorization_response'; - response: AuthResponse; - } - | { - type: 'authorization_response'; - error: Error; - }; +export type WebMessageResponse = _WebMessageResponse; diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts new file mode 100644 index 0000000000..5c5de6a890 --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { AuthResolverContext } from '@backstage/plugin-auth-node'; +import { AuthHandler } from '../../providers'; +import { OAuthResult } from '../oauth'; +import { PassportProfile } from '../passport/types'; +import { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler'; + +describe('adaptLegacyOAuthHandler', () => { + it('should pass through undefined', () => { + expect(adaptLegacyOAuthHandler(undefined)).toBeUndefined(); + }); + + it('should convert an old auth handler to a new profile transform', () => { + const authHandler: AuthHandler = jest.fn(); + const profileTransform = adaptLegacyOAuthHandler(authHandler); + + profileTransform?.( + { + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'token', + expiresInSeconds: 3, + scope: 'sco pe', + tokenType: 'bear', + idToken: 'id-token', + refreshToken: 'refresh-token', + }, + }, + { ctx: 'ctx' } as unknown as AuthResolverContext, + ); + + expect(authHandler).toHaveBeenCalledWith( + { + fullProfile: { id: 'id' }, + accessToken: 'token', + params: { + scope: 'sco pe', + id_token: 'id-token', + expires_in: 3, + token_type: 'bear', + }, + }, + { ctx: 'ctx' }, + ); + }); +}); diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts new file mode 100644 index 0000000000..3b8eca0a95 --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts @@ -0,0 +1,46 @@ +/* + * 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 { + OAuthAuthenticatorResult, + ProfileTransform, +} from '@backstage/plugin-auth-node'; +import { AuthHandler } from '../../providers'; +import { OAuthResult } from '../oauth'; +import { PassportProfile } from '../passport/types'; + +/** @internal */ +export function adaptLegacyOAuthHandler( + authHandler?: AuthHandler, +): ProfileTransform> | undefined { + return ( + authHandler && + (async (result, ctx) => + authHandler( + { + fullProfile: result.fullProfile, + accessToken: result.session.accessToken, + params: { + scope: result.session.scope, + id_token: result.session.idToken, + token_type: result.session.tokenType, + expires_in: result.session.expiresInSeconds!, + }, + }, + ctx, + )) + ); +} diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts new file mode 100644 index 0000000000..749cf96cbb --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { + AuthResolverContext, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver'; + +describe('adaptLegacyOAuthSignInResolver', () => { + it('should pass through undefined', () => { + expect(adaptLegacyOAuthSignInResolver(undefined)).toBeUndefined(); + }); + + it('should convert a legacy resolver to a new one', () => { + const legacyResolver = jest.fn(); + + const newResolver = adaptLegacyOAuthSignInResolver(legacyResolver); + + newResolver?.( + { + profile: { email: 'em@i.l' }, + result: { + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'token', + expiresInSeconds: 3, + scope: 'sco pe', + tokenType: 'bear', + idToken: 'id-token', + refreshToken: 'refresh-token', + }, + }, + }, + { ctx: 'ctx' } as unknown as AuthResolverContext, + ); + + expect(legacyResolver).toHaveBeenCalledWith( + { + profile: { email: 'em@i.l' }, + result: { + fullProfile: { id: 'id' }, + accessToken: 'token', + refreshToken: 'refresh-token', + params: { + scope: 'sco pe', + id_token: 'id-token', + expires_in: 3, + token_type: 'bear', + }, + }, + }, + { ctx: 'ctx' }, + ); + }); +}); diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts new file mode 100644 index 0000000000..e671a62d52 --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts @@ -0,0 +1,49 @@ +/* + * 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 { + OAuthAuthenticatorResult, + PassportProfile, + SignInResolver, +} from '@backstage/plugin-auth-node'; +import { OAuthResult } from '../oauth'; + +/** @internal */ +export function adaptLegacyOAuthSignInResolver( + signInResolver?: SignInResolver, +): SignInResolver> | undefined { + return ( + signInResolver && + (async (input, ctx) => + signInResolver( + { + profile: input.profile, + result: { + fullProfile: input.result.fullProfile, + accessToken: input.result.session.accessToken, + refreshToken: input.result.session.refreshToken, + params: { + scope: input.result.session.scope, + id_token: input.result.session.idToken, + token_type: input.result.session.tokenType, + expires_in: input.result.session.expiresInSeconds!, + }, + }, + }, + ctx, + )) + ); +} diff --git a/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.test.ts b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.test.ts new file mode 100644 index 0000000000..521dcf6395 --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { + AuthResolverContext, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { adaptOAuthSignInResolverToLegacy } from './adaptOAuthSignInResolverToLegacy'; + +describe('adaptOAuthSignInResolverToLegacy', () => { + it('should pass through an empty object', () => { + const legacyResolvers = adaptOAuthSignInResolverToLegacy({}); + expect(legacyResolvers).toEqual({}); + + // @ts-expect-error + legacyResolvers.missing?.(); + }); + + it('should adapt a collection of sign-in resolvers', () => { + const resolverA = jest.fn(); + const resolverB = jest.fn(); + + const legacyResolvers = adaptOAuthSignInResolverToLegacy({ + resolverA, + resolverB, + }); + + const legacyResolverA = legacyResolvers.resolverA(); + legacyResolverA( + { + profile: { email: 'em@i.l' }, + result: { + fullProfile: { id: 'id' } as PassportProfile, + accessToken: 'token', + refreshToken: 'refresh-token', + params: { + scope: 'sco pe', + id_token: 'id-token', + expires_in: 3, + token_type: 'bear', + }, + }, + }, + { ctx: 'ctx' } as unknown as AuthResolverContext, + ); + + expect(resolverA).toHaveBeenCalledWith( + { + profile: { email: 'em@i.l' }, + result: { + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'token', + expiresInSeconds: 3, + scope: 'sco pe', + tokenType: 'bear', + idToken: 'id-token', + refreshToken: 'refresh-token', + }, + }, + }, + { ctx: 'ctx' }, + ); + + expect(resolverB).not.toHaveBeenCalled(); + legacyResolvers.resolverB()( + { profile: {}, result: { params: {} } } as any, + {} as any, + ); + expect(resolverB).toHaveBeenCalled(); + }); +}); diff --git a/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.ts b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.ts new file mode 100644 index 0000000000..8be6049348 --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.ts @@ -0,0 +1,55 @@ +/* + * 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 { + OAuthAuthenticatorResult, + PassportProfile, + SignInResolver, +} from '@backstage/plugin-auth-node'; +import { OAuthResult } from '../oauth'; + +/** @internal */ +export function adaptOAuthSignInResolverToLegacy< + TKeys extends string, +>(resolvers: { + [key in TKeys]: SignInResolver>; +}): { [key in TKeys]: () => SignInResolver } { + const legacyResolvers = {} as { + [key in TKeys]: () => SignInResolver; + }; + for (const name of Object.keys(resolvers) as TKeys[]) { + const resolver = resolvers[name]; + legacyResolvers[name] = () => async (input, ctx) => + resolver( + { + profile: input.profile, + result: { + fullProfile: input.result.fullProfile, + session: { + accessToken: input.result.accessToken, + expiresInSeconds: input.result.params.expires_in, + scope: input.result.params.scope, + idToken: input.result.params.id_token, + tokenType: input.result.params.token_type ?? 'bearer', + refreshToken: input.result.refreshToken, + }, + }, + }, + ctx, + ); + } + return legacyResolvers; +} diff --git a/plugins/auth-backend/src/lib/legacy/index.ts b/plugins/auth-backend/src/lib/legacy/index.ts new file mode 100644 index 0000000000..8bd8b5c62e --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler'; +export { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver'; +export { adaptOAuthSignInResolverToLegacy } from './adaptOAuthSignInResolverToLegacy'; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index fb0750f0cb..9b4afb4b2b 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -50,7 +50,10 @@ import { prepareBackstageIdentityResponse } from '../../providers/prepareBacksta export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type OAuthAdapterOptions = { providerId: string; persistScopes?: boolean; @@ -61,7 +64,10 @@ export type OAuthAdapterOptions = { callbackUrl: string; }; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export class OAuthAdapter implements AuthProviderRouteHandlers { static fromConfig( config: AuthProviderConfig, diff --git a/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts index 1c6d45d0a4..c9244eb29e 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts @@ -14,84 +14,10 @@ * limitations under the License. */ -import express from 'express'; -import { Config } from '@backstage/config'; -import { InputError, NotFoundError } from '@backstage/errors'; -import { readState } from './helpers'; -import { AuthProviderRouteHandlers } from '../../providers/types'; +import { OAuthEnvironmentHandler as _OAuthEnvironmentHandler } from '@backstage/plugin-auth-node'; -/** @public */ -export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { - static mapConfig( - config: Config, - factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, - ) { - const envs = config.keys(); - const handlers = new Map(); - - for (const env of envs) { - const envConfig = config.getConfig(env); - const handler = factoryFunc(envConfig); - handlers.set(env, handler); - } - - return new OAuthEnvironmentHandler(handlers); - } - - constructor( - private readonly handlers: Map, - ) {} - - async start(req: express.Request, res: express.Response): Promise { - const provider = this.getProviderForEnv(req); - await provider.start(req, res); - } - - async frameHandler( - req: express.Request, - res: express.Response, - ): Promise { - const provider = this.getProviderForEnv(req); - await provider.frameHandler(req, res); - } - - async refresh(req: express.Request, res: express.Response): Promise { - const provider = this.getProviderForEnv(req); - await provider.refresh?.(req, res); - } - - async logout(req: express.Request, res: express.Response): Promise { - const provider = this.getProviderForEnv(req); - await provider.logout?.(req, res); - } - - private getRequestFromEnv(req: express.Request): string | undefined { - const reqEnv = req.query.env?.toString(); - if (reqEnv) { - return reqEnv; - } - const stateParams = req.query.state?.toString(); - if (!stateParams) { - return undefined; - } - const env = readState(stateParams).env; - return env; - } - - private getProviderForEnv(req: express.Request): AuthProviderRouteHandlers { - const env: string | undefined = this.getRequestFromEnv(req); - - if (!env) { - throw new InputError(`Must specify 'env' query to select environment`); - } - - const handler = this.handlers.get(env); - if (!handler) { - throw new NotFoundError( - `No configuration available for the '${env}' environment of this provider.`, - ); - } - - return handler; - } -} +/** + * @public + * @deprecated import from `@backstage/plugin-auth-node` instead + */ +export const OAuthEnvironmentHandler = _OAuthEnvironmentHandler; diff --git a/plugins/auth-backend/src/lib/oauth/helpers.test.ts b/plugins/auth-backend/src/lib/oauth/helpers.test.ts index 58a3dcab05..c8db34e2cc 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.test.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.test.ts @@ -76,7 +76,7 @@ describe('OAuthProvider Utils', () => { } as unknown as express.Request; expect(() => { verifyNonce(mockRequest, 'providera'); - }).toThrow('Invalid state passed via request'); + }).toThrow('OAuth state is invalid, missing env'); }); it('should throw error if nonce mismatch', () => { diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index 2b4e50b477..5e67b072e3 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -16,36 +16,28 @@ import express from 'express'; import { OAuthState } from './types'; -import pickBy from 'lodash/pickBy'; import { CookieConfigurer } from '../../providers/types'; +import { + decodeOAuthState, + encodeOAuthState, +} from '@backstage/plugin-auth-node'; -/** @public */ -export const readState = (stateString: string): OAuthState => { - const state = Object.fromEntries( - new URLSearchParams(Buffer.from(stateString, 'hex').toString('utf-8')), - ); - if ( - !state.nonce || - !state.env || - state.nonce?.length === 0 || - state.env?.length === 0 - ) { - throw Error(`Invalid state passed via request`); - } +/** + * @public + * @deprecated Use `decodeOAuthState` from `@backstage/plugin-auth-node` instead + */ +export const readState = decodeOAuthState; - return state as OAuthState; -}; +/** + * @public + * @deprecated Use `encodeOAuthState` from `@backstage/plugin-auth-node` instead + */ +export const encodeState = encodeOAuthState; -/** @public */ -export const encodeState = (state: OAuthState): string => { - const stateString = new URLSearchParams( - pickBy(state, value => value !== undefined), - ).toString(); - - return Buffer.from(stateString, 'utf-8').toString('hex'); -}; - -/** @public */ +/** + * @public + * @deprecated Use inline logic to make sure the session and state nonce matches instead. + */ export const verifyNonce = (req: express.Request, providerId: string) => { const cookieNonce = req.cookies[`${providerId}-nonce`]; const state: OAuthState = readState(req.query.state?.toString() ?? ''); diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index e960af2988..b7205c9b85 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -16,13 +16,17 @@ import express from 'express'; import { Profile as PassportProfile } from 'passport'; -import { BackstageSignInResult } from '@backstage/plugin-auth-node'; +import { + BackstageSignInResult, + OAuthState as _OAuthState, +} from '@backstage/plugin-auth-node'; import { OAuthStartResponse, ProfileInfo } from '../../providers/types'; /** * Common options for passport.js-based OAuth providers * * @public + * @deprecated No longer in use */ export type OAuthProviderOptions = { /** @@ -39,12 +43,16 @@ export type OAuthProviderOptions = { callbackUrl: string; }; -/** @public */ +/** + * @public + * @deprecated Use `OAuthAuthenticatorResult` from `@backstage/plugin-auth-node` instead + */ export type OAuthResult = { fullProfile: PassportProfile; params: { id_token?: string; scope: string; + token_type?: string; expires_in: number; }; accessToken: string; @@ -52,9 +60,8 @@ export type OAuthResult = { }; /** - * The expected response from an OAuth flow. - * * @public + * @deprecated Use `ClientAuthResponse` from `@backstage/plugin-auth-node` instead */ export type OAuthResponse = { profile: ProfileInfo; @@ -62,7 +69,10 @@ export type OAuthResponse = { backstageIdentity?: BackstageSignInResult; }; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type OAuthProviderInfo = { /** * An access token issued for the signed in user. @@ -82,41 +92,41 @@ export type OAuthProviderInfo = { scope: string; }; -/** @public */ -export type OAuthState = { - /* A type for the serialized value in the `state` parameter of the OAuth authorization flow - */ - nonce: string; - env: string; - origin?: string; - scope?: string; - redirectUrl?: string; - flow?: string; -}; +/** + * @public + * @deprecated import from `@backstage/plugin-auth-node` instead + */ +export type OAuthState = _OAuthState; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type OAuthStartRequest = express.Request<{}> & { scope: string; state: OAuthState; }; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type OAuthRefreshRequest = express.Request<{}> & { scope: string; refreshToken: string; }; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type OAuthLogoutRequest = express.Request<{}> & { refreshToken: string; }; /** - * Any OAuth provider needs to implement this interface which has provider specific - * handlers for different methods to perform authentication, get access tokens, - * refresh tokens and perform sign out. - * * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead */ export interface OAuthHandlers { /** diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index cbc2439563..7d22526193 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -24,7 +24,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { ConflictError, InputError, NotFoundError } from '@backstage/errors'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { TokenIssuer, TokenParams } from '../../identity/types'; import { AuthResolverContext } from '../../providers'; import { AuthResolverCatalogUserQuery } from '../../providers/types'; @@ -54,7 +54,7 @@ export function getDefaultOwnershipEntityRefs(entity: Entity) { */ export class CatalogAuthResolverContext implements AuthResolverContext { static create(options: { - logger: Logger; + logger: LoggerService; catalogApi: CatalogApi; tokenIssuer: TokenIssuer; tokenManager: TokenManager; @@ -73,7 +73,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext { } private constructor( - public readonly logger: Logger, + public readonly logger: LoggerService, public readonly tokenIssuer: TokenIssuer, public readonly catalogIdentityClient: CatalogIdentityClient, private readonly catalogApi: CatalogApi, diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts deleted file mode 100644 index 0162366f61..0000000000 --- a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConflictError } from '@backstage/errors'; -import { OAuth2Client } from 'google-auth-library'; -import { createTokenValidator, parseRequestToken } from './helpers'; - -const validJwt = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo'; - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('helpers', () => { - describe('createTokenValidator', () => { - it('runs the happy path', async () => { - const mockClient = { - getIapPublicKeys: async () => ({ pubkeys: '' }), - verifySignedJwtWithCertsAsync: async () => ({ - getPayload: () => ({ sub: 's', email: 'e@mail.com' }), - }), - }; - const validator = createTokenValidator( - 'a', - mockClient as unknown as OAuth2Client, - ); - await expect(validator(validJwt)).resolves.toMatchObject({ - sub: 's', - email: 'e@mail.com', - }); - }); - - it('throws if the client throws', async () => { - const mockClient = { - getIapPublicKeys: async () => { - throw new TypeError('bam'); - }, - }; - const validator = createTokenValidator( - 'a', - mockClient as unknown as OAuth2Client, - ); - await expect(validator(validJwt)).rejects.toThrow(TypeError); - }); - - it('rejects empty payload', async () => { - const mockClient = { - getIapPublicKeys: async () => ({ pubkeys: '' }), - verifySignedJwtWithCertsAsync: async () => ({ - getPayload: () => undefined, - }), - }; - const validator = createTokenValidator( - 'a', - mockClient as unknown as OAuth2Client, - ); - await expect(validator(validJwt)).rejects.toMatchObject({ - name: 'TypeError', - message: 'Token had no payload', - }); - }); - }); - - describe('parseRequestToken', () => { - it('runs the happy path', async () => { - await expect( - parseRequestToken( - validJwt, - async () => ({ sub: 's', email: 'e@mail.com' } as any), - ), - ).resolves.toMatchObject({ - iapToken: { - sub: 's', - email: 'e@mail.com', - }, - }); - }); - - it('rejects bad tokens', async () => { - await expect( - parseRequestToken(7, undefined as any), - ).rejects.toMatchObject({ - name: 'AuthenticationError', - message: 'Missing Google IAP header', - }); - await expect( - parseRequestToken(undefined, undefined as any), - ).rejects.toMatchObject({ - name: 'AuthenticationError', - message: 'Missing Google IAP header', - }); - await expect( - parseRequestToken('', undefined as any), - ).rejects.toMatchObject({ - name: 'AuthenticationError', - message: 'Missing Google IAP header', - }); - }); - - it('translates validator errors', async () => { - await expect( - parseRequestToken(validJwt, async () => { - throw new ConflictError('Ouch'); - }), - ).rejects.toMatchObject({ - name: 'AuthenticationError', - message: 'Google IAP token verification failed, ConflictError: Ouch', - }); - }); - - it('rejects bad token payloads', async () => { - await expect( - parseRequestToken(validJwt, async () => ({ sub: 'a' } as any)), - ).rejects.toMatchObject({ - name: 'AuthenticationError', - message: 'Google IAP token payload is missing sub and/or email claim', - }); - }); - }); -}); diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.ts deleted file mode 100644 index 26d9a8c904..0000000000 --- a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AuthenticationError } from '@backstage/errors'; -import { OAuth2Client, TokenPayload } from 'google-auth-library'; -import { AuthHandler } from '../types'; -import { GcpIapResult } from './types'; - -export function createTokenValidator( - audience: string, - mockClient?: OAuth2Client, -): (token: string) => Promise { - const client = mockClient ?? new OAuth2Client(); - - return async function tokenValidator(token) { - // TODO(freben): Rate limit the public key reads. It may be sensible to - // cache these for some reasonable time rather than asking for the public - // keys on every single sign-in. But since the rate of events here is so - // slow, I decided to keep it simple for now. - const response = await client.getIapPublicKeys(); - const ticket = await client.verifySignedJwtWithCertsAsync( - token, - response.pubkeys, - audience, - ['https://cloud.google.com/iap'], - ); - - const payload = ticket.getPayload(); - if (!payload) { - throw new TypeError('Token had no payload'); - } - - return payload; - }; -} - -export async function parseRequestToken( - jwtToken: unknown, - tokenValidator: (token: string) => Promise, -): Promise { - if (typeof jwtToken !== 'string' || !jwtToken) { - throw new AuthenticationError('Missing Google IAP header'); - } - - let payload: TokenPayload; - try { - payload = await tokenValidator(jwtToken); - } catch (e) { - throw new AuthenticationError(`Google IAP token verification failed, ${e}`); - } - - if (!payload.sub || !payload.email) { - throw new AuthenticationError( - 'Google IAP token payload is missing sub and/or email claim', - ); - } - - return { - iapToken: { - ...payload, - sub: payload.sub, - email: payload.email, - }, - }; -} - -export const defaultAuthHandler: AuthHandler = async ({ - iapToken, -}) => ({ profile: { email: iapToken.email } }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts deleted file mode 100644 index c01db3a853..0000000000 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import express from 'express'; -import request from 'supertest'; -import { AuthResolverContext } from '../types'; -import { GcpIapProvider } from './provider'; -import { DEFAULT_IAP_JWT_HEADER } from './types'; - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('GcpIapProvider', () => { - const authHandler = jest.fn(); - const signInResolver = jest.fn(); - const tokenValidator = jest.fn(); - - it.each([undefined, 'x-custom-header'])( - 'runs the happy path', - async jwtHeader => { - const provider = new GcpIapProvider({ - authHandler, - signInResolver, - tokenValidator, - resolverContext: {} as AuthResolverContext, - jwtHeader: jwtHeader, - }); - - // { "sub": "user:default/me", "ent": ["group:default/home"] } - const backstageToken = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc'; - const iapToken = { sub: 's', email: 'e@mail.com' }; - - authHandler.mockResolvedValueOnce({ email: 'e@mail.com' }); - signInResolver.mockResolvedValueOnce({ token: backstageToken }); - tokenValidator.mockResolvedValueOnce(iapToken); - - const app = express(); - app.use('/refresh', provider.refresh.bind(provider)); - - const header = jwtHeader || DEFAULT_IAP_JWT_HEADER; - const response = await request(app).get('/refresh').set(header, 'token'); - - expect(response.status).toBe(200); - expect(response.get('content-type')).toBe( - 'application/json; charset=utf-8', - ); - expect(response.body).toEqual({ - backstageIdentity: { - token: backstageToken, - identity: { - type: 'user', - userEntityRef: 'user:default/me', - ownershipEntityRefs: ['group:default/home'], - }, - }, - providerInfo: { iapToken }, - }); - }, - ); -}); diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index 7e77853737..db4d9195b2 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -14,70 +14,11 @@ * limitations under the License. */ -import express from 'express'; -import { TokenPayload } from 'google-auth-library'; +import { gcpIapAuthenticator } from '@backstage/plugin-auth-backend-module-gcp-iap-provider'; +import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; -import { - AuthHandler, - AuthProviderRouteHandlers, - AuthResolverContext, - SignInResolver, -} from '../types'; -import { - createTokenValidator, - defaultAuthHandler, - parseRequestToken, -} from './helpers'; -import { GcpIapResponse, GcpIapResult, DEFAULT_IAP_JWT_HEADER } from './types'; - -export class GcpIapProvider implements AuthProviderRouteHandlers { - private readonly authHandler: AuthHandler; - private readonly signInResolver: SignInResolver; - private readonly tokenValidator: (token: string) => Promise; - private readonly resolverContext: AuthResolverContext; - private readonly jwtHeader: string; - - constructor(options: { - authHandler: AuthHandler; - signInResolver: SignInResolver; - tokenValidator: (token: string) => Promise; - resolverContext: AuthResolverContext; - jwtHeader?: string; - }) { - this.authHandler = options.authHandler; - this.signInResolver = options.signInResolver; - this.tokenValidator = options.tokenValidator; - this.resolverContext = options.resolverContext; - this.jwtHeader = options?.jwtHeader || DEFAULT_IAP_JWT_HEADER; - } - - async start() {} - - async frameHandler() {} - - async refresh(req: express.Request, res: express.Response): Promise { - const result = await parseRequestToken( - req.header(this.jwtHeader), - this.tokenValidator, - ); - - const { profile } = await this.authHandler(result, this.resolverContext); - - const backstageIdentity = await this.signInResolver( - { profile, result }, - this.resolverContext, - ); - - const response: GcpIapResponse = { - providerInfo: { iapToken: result.iapToken }, - profile, - backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity), - }; - - res.json(response); - } -} +import { AuthHandler, SignInResolver } from '../types'; +import { GcpIapResult } from './types'; /** * Auth provider integration for Google Identity-Aware Proxy auth @@ -104,21 +45,10 @@ export const gcpIap = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return ({ config, resolverContext }) => { - const audience = config.getString('audience'); - const jwtHeader = config.getOptionalString('jwtHeader'); - - const authHandler = options.authHandler ?? defaultAuthHandler; - const signInResolver = options.signIn.resolver; - const tokenValidator = createTokenValidator(audience); - - return new GcpIapProvider({ - authHandler, - signInResolver, - tokenValidator, - resolverContext, - jwtHeader, - }); - }; + return createProxyAuthProviderFactory({ + authenticator: gcpIapAuthenticator, + profileTransform: options?.authHandler, + signInResolver: options?.signIn?.resolver, + }); }, }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/types.ts b/plugins/auth-backend/src/providers/gcp-iap/types.ts index 6519de64ac..b1f69318fc 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/types.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/types.ts @@ -14,58 +14,24 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/types'; -import { AuthResponse } from '../types'; - -/** - * The header name used by the IAP. - */ -export const DEFAULT_IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; +import { + GcpIapTokenInfo as _GcpIapTokenInfo, + GcpIapResult as _GcpIapResult, +} from '@backstage/plugin-auth-backend-module-gcp-iap-provider'; /** * The data extracted from an IAP token. * * @public + * @deprecated import from `@backstage/plugin-auth-backend-module-gcp-iap-provider` instead */ -export type GcpIapTokenInfo = { - /** - * The unique, stable identifier for the user. - */ - sub: string; - /** - * User email address. - */ - email: string; - /** - * Other fields. - */ - [key: string]: JsonValue; -}; +export type GcpIapTokenInfo = _GcpIapTokenInfo; /** * The result of the initial auth challenge. This is the input to the auth * callbacks. * * @public + * @deprecated import from `@backstage/plugin-auth-backend-module-gcp-iap-provider` instead */ -export type GcpIapResult = { - /** - * The data extracted from the IAP token header. - */ - iapToken: GcpIapTokenInfo; -}; - -/** - * The provider info to return to the frontend. - */ -export type GcpIapProviderInfo = { - /** - * The data extracted from the IAP token header. - */ - iapToken: GcpIapTokenInfo; -}; - -/** - * The shape of the response to return to callers. - */ -export type GcpIapResponse = AuthResponse; +export type GcpIapResult = _GcpIapResult; diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts deleted file mode 100644 index ff750223d3..0000000000 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ /dev/null @@ -1,410 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Profile as PassportProfile } from 'passport'; -import { GithubAuthProvider, GithubOAuthResult, github } from './provider'; -import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; -import { OAuthStartRequest, encodeState } from '../../lib/oauth'; -import { AuthResolverContext } from '../types'; - -jest.mock('../../lib/passport/PassportStrategyHelper', () => { - return { - ...jest.requireActual('../../lib/passport/PassportStrategyHelper'), - executeFrameHandlerStrategy: jest.fn(), - executeRefreshTokenStrategy: jest.fn(), - executeFetchUserProfileStrategy: jest.fn(), - }; -}); - -const mockFrameHandler = jest.spyOn( - helpers, - 'executeFrameHandlerStrategy', -) as unknown as jest.MockedFunction< - () => Promise<{ - result: GithubOAuthResult; - privateInfo: { refreshToken?: string }; - }> ->; - -describe('GithubAuthProvider', () => { - const provider = new GithubAuthProvider({ - resolverContext: { - signInWithCatalogUser: jest.fn(({ entityRef }) => ({ - token: `token-for-user:${entityRef.name}`, - })), - } as unknown as AuthResolverContext, - signInResolver: github.resolvers.usernameMatchingUserEntityName(), - authHandler: async ({ fullProfile }) => ({ - profile: makeProfileInfo(fullProfile), - }), - stateEncoder: async (req: OAuthStartRequest) => ({ - encodedState: encodeState(req.state), - }), - callbackUrl: 'mock', - clientId: 'mock', - clientSecret: 'mock', - }); - - describe('should transform to type OAuthResponse', () => { - it('when all fields are present, it should be able to map them', async () => { - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const fullProfile = { - id: 'uid-123', - username: 'jimmymarkum', - provider: 'github', - displayName: 'Jimmy Markum', - emails: [ - { - value: 'jimmymarkum@gmail.com', - }, - ], - photos: [ - { - value: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - ], - }; - - const params = { - scope: 'read:scope', - }; - - const expected = { - backstageIdentity: { - token: 'token-for-user:jimmymarkum', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'read:scope', - expiresInSeconds: 3600, - }, - profile: { - email: 'jimmymarkum@gmail.com', - displayName: 'Jimmy Markum', - picture: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('when "email" is missing, it should be able to create the profile without it', async () => { - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const fullProfile = { - id: 'uid-123', - username: 'jimmymarkum', - provider: 'github', - displayName: 'Jimmy Markum', - emails: null, - photos: [ - { - value: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - ], - } as unknown as PassportProfile; - - const params = { - scope: 'read:scope', - }; - - const expected = { - backstageIdentity: { - token: 'token-for-user:jimmymarkum', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'read:scope', - expiresInSeconds: 3600, - }, - profile: { - displayName: 'Jimmy Markum', - picture: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('when "displayName" is missing, it should be able to create the profile and map "displayName" with "username"', async () => { - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const fullProfile = { - id: 'uid-123', - username: 'jimmymarkum', - provider: 'github', - displayName: null, - emails: null, - photos: [ - { - value: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - ], - } as unknown as PassportProfile; - - const params = { - scope: 'read:scope', - }; - const expected = { - backstageIdentity: { - token: 'token-for-user:jimmymarkum', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'read:scope', - expiresInSeconds: 3600, - }, - profile: { - displayName: 'jimmymarkum', - picture: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('when "photos" is missing, it should be able to create the profile without it', async () => { - const accessToken = - 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe'; - const fullProfile = { - id: 'ipd12039', - username: 'daveboyle', - provider: 'github', - displayName: 'Dave Boyle', - emails: [ - { - value: 'daveboyle@github.org', - }, - ], - }; - - const params = { - scope: 'read:user', - }; - - const expected = { - backstageIdentity: { - token: 'token-for-user:daveboyle', - }, - providerInfo: { - accessToken: - 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', - scope: 'read:user', - expiresInSeconds: 3600, - }, - profile: { - displayName: 'Dave Boyle', - email: 'daveboyle@github.org', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('should forward a refresh token', async () => { - mockFrameHandler.mockResolvedValueOnce({ - result: { - fullProfile: { - id: 'ipd12039', - username: 'daveboyle', - provider: 'github', - displayName: 'Dave Boyle', - }, - accessToken: 'a.b.c', - params: { - scope: 'read:user', - expires_in: '123', - }, - }, - privateInfo: { refreshToken: 'refresh-me' }, - }); - - const response = await provider.handler({} as any); - - expect(response).toEqual({ - response: { - backstageIdentity: { - token: 'token-for-user:daveboyle', - }, - providerInfo: { - accessToken: 'a.b.c', - scope: 'read:user', - expiresInSeconds: 123, - }, - profile: { - displayName: 'Dave Boyle', - }, - }, - refreshToken: 'refresh-me', - }); - }); - - it('should fail if username is not available', async () => { - mockFrameHandler.mockResolvedValueOnce({ - result: { - fullProfile: { - id: 'ipd12039', - provider: 'github', - displayName: 'Dave Boyle', - }, - accessToken: 'a.b.c', - params: { - scope: 'read:user', - expires_in: '123', - }, - }, - privateInfo: { refreshToken: 'refresh-me' }, - }); - - await expect(provider.handler({} as any)).rejects.toThrow( - 'GitHub user profile does not contain a username', - ); - }); - - it('should forward a new refresh token on refresh', async () => { - const mockRefreshToken = jest.spyOn( - helpers, - 'executeRefreshTokenStrategy', - ) as unknown as jest.MockedFunction<() => Promise<{}>>; - - mockRefreshToken.mockResolvedValueOnce({ - accessToken: 'a.b.c', - refreshToken: 'dont-forget-to-send-refresh', - params: { - id_token: 'my-id', - expires_in: '123', - scope: 'read_user', - }, - }); - - const mockUserProfile = jest.spyOn( - helpers, - 'executeFetchUserProfileStrategy', - ) as unknown as jest.MockedFunction<() => Promise>; - - mockUserProfile.mockResolvedValueOnce({ - id: 'mockid', - username: 'mockuser', - provider: 'github', - displayName: 'Mocked User', - emails: [ - { - value: 'mockuser@gmail.com', - }, - ], - }); - - const result = await provider.refresh({ scope: 'actual-scope' } as any); - - expect(result).toEqual({ - response: { - backstageIdentity: { - token: 'token-for-user:mockuser', - }, - profile: { - displayName: 'Mocked User', - email: 'mockuser@gmail.com', - picture: undefined, - }, - providerInfo: { - accessToken: 'a.b.c', - expiresInSeconds: 123, - scope: 'actual-scope', - }, - }, - refreshToken: 'dont-forget-to-send-refresh', - }); - - mockRefreshToken.mockRestore(); - mockUserProfile.mockRestore(); - }); - - it('should use access token as refresh token', async () => { - const mockUserProfile = jest.spyOn( - helpers, - 'executeFetchUserProfileStrategy', - ) as unknown as jest.MockedFunction<() => Promise>; - - mockUserProfile.mockResolvedValueOnce({ - id: 'mockid', - username: 'mockuser', - provider: 'github', - displayName: 'Mocked User', - emails: [ - { - value: 'mockuser@gmail.com', - }, - ], - }); - - const result = await provider.refresh({ - refreshToken: 'access-token.le-token', - scope: 'the-scope', - } as any); - - expect(mockUserProfile).toHaveBeenCalledTimes(1); - expect(mockUserProfile).toHaveBeenCalledWith( - expect.anything(), - 'le-token', - ); - expect(result).toEqual({ - response: { - backstageIdentity: { - token: 'token-for-user:mockuser', - }, - profile: { - displayName: 'Mocked User', - email: 'mockuser@gmail.com', - picture: undefined, - }, - providerInfo: { - accessToken: 'le-token', - expiresInSeconds: 3600, - scope: 'the-scope', - }, - }, - refreshToken: 'access-token.le-token', - }); - - mockUserProfile.mockRestore(); - }); - }); -}); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 11a71ca018..51595b0428 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -14,41 +14,16 @@ * limitations under the License. */ -import express from 'express'; import { Profile as PassportProfile } from 'passport'; -import { Strategy as GithubStrategy } from 'passport-github2'; -import { - executeFetchUserProfileStrategy, - executeFrameHandlerStrategy, - executeRedirectStrategy, - executeRefreshTokenStrategy, - makeProfileInfo, - PassportDoneCallback, -} from '../../lib/passport'; -import { - OAuthStartResponse, - AuthHandler, - SignInResolver, - StateEncoder, - AuthResolverContext, -} from '../types'; -import { - OAuthAdapter, - OAuthProviderOptions, - OAuthHandlers, - OAuthEnvironmentHandler, - OAuthStartRequest, - encodeState, - OAuthRefreshRequest, -} from '../../lib/oauth'; +import { AuthHandler, StateEncoder } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { BACKSTAGE_SESSION_EXPIRATION } from '../../lib/session'; - -const ACCESS_TOKEN_PREFIX = 'access-token.'; - -type PrivateInfo = { - refreshToken?: string; -}; +import { + createOAuthProviderFactory, + OAuthAuthenticatorResult, + ProfileTransform, + SignInResolver, +} from '@backstage/plugin-auth-node'; +import { githubAuthenticator } from '@backstage/plugin-auth-backend-module-github-provider'; /** @public */ export type GithubOAuthResult = { @@ -62,170 +37,6 @@ export type GithubOAuthResult = { refreshToken?: string; }; -export type GithubAuthProviderOptions = OAuthProviderOptions & { - tokenUrl?: string; - userProfileUrl?: string; - authorizationUrl?: string; - signInResolver?: SignInResolver; - authHandler: AuthHandler; - stateEncoder: StateEncoder; - resolverContext: AuthResolverContext; -}; - -export class GithubAuthProvider implements OAuthHandlers { - private readonly _strategy: GithubStrategy; - private readonly signInResolver?: SignInResolver; - private readonly authHandler: AuthHandler; - private readonly resolverContext: AuthResolverContext; - private readonly stateEncoder: StateEncoder; - - constructor(options: GithubAuthProviderOptions) { - this.signInResolver = options.signInResolver; - this.authHandler = options.authHandler; - this.stateEncoder = options.stateEncoder; - this.resolverContext = options.resolverContext; - this._strategy = new GithubStrategy( - { - clientID: options.clientId, - clientSecret: options.clientSecret, - callbackURL: options.callbackUrl, - tokenURL: options.tokenUrl, - userProfileURL: options.userProfileUrl, - authorizationURL: options.authorizationUrl, - }, - ( - accessToken: any, - refreshToken: any, - params: any, - fullProfile: any, - done: PassportDoneCallback, - ) => { - done(undefined, { fullProfile, params, accessToken }, { refreshToken }); - }, - ); - } - - async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this._strategy, { - scope: req.scope, - state: (await this.stateEncoder(req)).encodedState, - }); - } - - async handler(req: express.Request) { - const { result, privateInfo } = await executeFrameHandlerStrategy< - GithubOAuthResult, - PrivateInfo - >(req, this._strategy); - - let refreshToken = privateInfo.refreshToken; - - // If we do not have a real refresh token and we have a non-expiring - // access token, then we use that as our refresh token. - if (!refreshToken && !result.params.expires_in) { - refreshToken = ACCESS_TOKEN_PREFIX + result.accessToken; - } - - return { - response: await this.handleResult(result), - refreshToken, - }; - } - - async refresh(req: OAuthRefreshRequest) { - // We've enable persisting scope in the OAuth provider, so scope here will - // be whatever was stored in the cookie - const { scope, refreshToken } = req; - - // This is the OAuth App flow. A non-expiring access token is stored in the - // refresh token cookie. We use that token to fetch the user profile and - // refresh the Backstage session when needed. - if (refreshToken?.startsWith(ACCESS_TOKEN_PREFIX)) { - const accessToken = refreshToken.slice(ACCESS_TOKEN_PREFIX.length); - - const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, - accessToken, - ).catch(error => { - if (error.oauthError?.statusCode === 401) { - throw new Error('Invalid access token'); - } - throw error; - }); - - return { - response: await this.handleResult({ - fullProfile, - params: { scope }, - accessToken, - }), - refreshToken, - }; - } - - // This is the App flow, which is close to a standard OAuth refresh flow. It has a - // pretty long session expiration, and it also ignores the requested scope, instead - // just allowing access to whatever is configured as part of the app installation. - const result = await executeRefreshTokenStrategy( - this._strategy, - refreshToken, - scope, - ); - return { - response: await this.handleResult({ - fullProfile: await executeFetchUserProfileStrategy( - this._strategy, - result.accessToken, - ), - params: { ...result.params, scope }, - accessToken: result.accessToken, - }), - refreshToken: result.refreshToken, - }; - } - - private async handleResult(result: GithubOAuthResult) { - const { profile } = await this.authHandler(result, this.resolverContext); - - const expiresInStr = result.params.expires_in; - let expiresInSeconds = - expiresInStr === undefined ? undefined : Number(expiresInStr); - - let backstageIdentity = undefined; - - if (this.signInResolver) { - backstageIdentity = await this.signInResolver( - { - result, - profile, - }, - this.resolverContext, - ); - - // GitHub sessions last longer than Backstage sessions, so if we're using - // GitHub for sign-in, then we need to expire the sessions earlier - if (expiresInSeconds) { - expiresInSeconds = Math.min( - expiresInSeconds, - BACKSTAGE_SESSION_EXPIRATION, - ); - } else { - expiresInSeconds = BACKSTAGE_SESSION_EXPIRATION; - } - } - - return { - backstageIdentity, - providerInfo: { - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds, - }, - profile, - }; - } -} - /** * Auth provider integration for GitHub auth * @@ -267,60 +78,55 @@ export const github = createAuthProviderIntegration({ */ stateEncoder?: StateEncoder; }) { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const enterpriseInstanceUrl = envConfig - .getOptionalString('enterpriseInstanceUrl') - ?.replace(/\/$/, ''); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const authorizationUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/authorize` - : undefined; - const tokenUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/access_token` - : undefined; - const userProfileUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/api/v3/user` - : undefined; - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile }) => ({ - profile: makeProfileInfo(fullProfile), - }); - - const stateEncoder: StateEncoder = - options?.stateEncoder ?? - (async ( - req: OAuthStartRequest, - ): Promise<{ encodedState: string }> => { - return { encodedState: encodeState(req.state) }; - }); - - const provider = new GithubAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenUrl, - userProfileUrl, - authorizationUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - stateEncoder, - resolverContext, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - persistScopes: true, - providerId, - callbackUrl, - }); - }); + const authHandler = options?.authHandler; + const signInResolver = options?.signIn?.resolver; + return createOAuthProviderFactory({ + authenticator: githubAuthenticator, + profileTransform: + authHandler && + ((async (result, ctx) => + authHandler!( + { + fullProfile: result.fullProfile, + accessToken: result.session.accessToken, + params: { + scope: result.session.scope, + expires_in: result.session.expiresInSeconds + ? String(result.session.expiresInSeconds) + : '', + refresh_token_expires_in: result.session + .refreshTokenExpiresInSeconds + ? String(result.session.refreshTokenExpiresInSeconds) + : '', + }, + }, + ctx, + )) as ProfileTransform>), + signInResolver: + signInResolver && + ((async ({ profile, result }, ctx) => + signInResolver( + { + profile: profile, + result: { + fullProfile: result.fullProfile, + accessToken: result.session.accessToken, + refreshToken: result.session.refreshToken, + params: { + scope: result.session.scope, + expires_in: result.session.expiresInSeconds + ? String(result.session.expiresInSeconds) + : '', + refresh_token_expires_in: result.session + .refreshTokenExpiresInSeconds + ? String(result.session.refreshTokenExpiresInSeconds) + : '', + }, + }, + }, + ctx, + )) as SignInResolver>), + }); }, resolvers: { /** diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts deleted file mode 100644 index f9e1442661..0000000000 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - GitlabAuthProvider, - gitlabUsernameEntityNameSignInResolver, -} from './provider'; -import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { PassportProfile } from '../../lib/passport/types'; -import { OAuthResult } from '../../lib/oauth'; -import { AuthResolverContext } from '../types'; - -jest.mock('../../lib/passport/PassportStrategyHelper', () => { - return { - executeFrameHandlerStrategy: jest.fn(), - executeRefreshTokenStrategy: jest.fn(), - executeFetchUserProfileStrategy: jest.fn(), - }; -}); - -const mockFrameHandler = jest.spyOn( - helpers, - 'executeFrameHandlerStrategy', -) as unknown as jest.MockedFunction<() => Promise<{ result: OAuthResult }>>; - -describe('GitlabAuthProvider', () => { - const provider = new GitlabAuthProvider({ - clientId: 'mock', - clientSecret: 'mock', - callbackUrl: 'mock', - baseUrl: 'mock', - resolverContext: { - signInWithCatalogUser: jest.fn(async ({ entityRef }) => ({ - token: `token-for-user:${entityRef.name}`, - })), - } as unknown as AuthResolverContext, - authHandler: async ({ fullProfile }) => ({ - profile: { - email: fullProfile.emails![0]!.value, - displayName: fullProfile.displayName, - picture: 'http://gitlab.com/lols', - }, - }), - signInResolver: gitlabUsernameEntityNameSignInResolver, - }); - - it('should transform to type OAuthResponse', async () => { - const tests = [ - { - input: { - result: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - fullProfile: { - id: 'uid-123', - username: 'jimmymarkum', - provider: 'gitlab', - displayName: 'Jimmy Markum', - emails: [ - { - value: 'jimmymarkum@gmail.com', - }, - ], - avatarUrl: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - params: { - scope: 'user_read write_repository', - expires_in: 100, - }, - }, - privateInfo: { - refreshToken: 'gacn9jdgm19me19xasczxcm9n7', - }, - }, - expect: { - backstageIdentity: { - token: 'token-for-user:jimmymarkum', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - expiresInSeconds: 100, - scope: 'user_read write_repository', - idToken: undefined, - }, - profile: { - email: 'jimmymarkum@gmail.com', - displayName: 'Jimmy Markum', - picture: 'http://gitlab.com/lols', - }, - }, - }, - { - input: { - result: { - accessToken: - 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', - fullProfile: { - id: 'ipd12039', - username: 'daveboyle', - provider: 'gitlab', - displayName: 'Dave Boyle', - emails: [ - { - value: 'daveboyle@gitlab.org', - }, - ], - }, - params: { - scope: 'read_repository', - expires_in: 200, - }, - }, - privateInfo: { - refreshToken: 'gacn96f3y6y5jdgm19mec348nqrty719xasczf356yxcm9n7', - }, - }, - expect: { - backstageIdentity: { - token: 'token-for-user:daveboyle', - }, - providerInfo: { - accessToken: - 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', - expiresInSeconds: 200, - idToken: undefined, - scope: 'read_repository', - }, - profile: { - displayName: 'Dave Boyle', - email: 'daveboyle@gitlab.org', - picture: 'http://gitlab.com/lols', - }, - }, - }, - ]; - - for (const test of tests) { - mockFrameHandler.mockResolvedValueOnce(test.input); - const { response } = await provider.handler({} as any); - expect(response).toEqual(test.expect); - } - }); - - it('should forward a new refresh token on refresh', async () => { - const mockRefreshToken = jest.spyOn( - helpers, - 'executeRefreshTokenStrategy', - ) as unknown as jest.MockedFunction<() => Promise<{}>>; - - mockRefreshToken.mockResolvedValueOnce({ - accessToken: 'a.b.c', - refreshToken: 'dont-forget-to-send-refresh', - params: { - id_token: 'my-id', - scope: 'read_user', - }, - }); - - const mockUserProfile = jest.spyOn( - helpers, - 'executeFetchUserProfileStrategy', - ) as unknown as jest.MockedFunction<() => Promise>; - - mockUserProfile.mockResolvedValueOnce({ - id: 'uid-my-id', - username: 'mockuser', - provider: 'gitlab', - displayName: 'Mocked User', - emails: [ - { - value: 'mockuser@gmail.com', - }, - ], - }); - - const result = await provider.refresh({} as any); - - expect(result).toEqual({ - response: { - backstageIdentity: { - token: 'token-for-user:mockuser', - }, - profile: { - displayName: 'Mocked User', - email: 'mockuser@gmail.com', - picture: 'http://gitlab.com/lols', - }, - providerInfo: { - accessToken: 'a.b.c', - idToken: 'my-id', - scope: 'read_user', - }, - }, - refreshToken: 'dont-forget-to-send-refresh', - }); - }); -}); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 31efafa4ee..2551d334ec 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -14,172 +14,15 @@ * limitations under the License. */ -import express from 'express'; -import { Strategy as GitlabStrategy } from 'passport-gitlab2'; -import { - executeRedirectStrategy, - executeFrameHandlerStrategy, - executeRefreshTokenStrategy, - executeFetchUserProfileStrategy, - makeProfileInfo, - PassportDoneCallback, -} from '../../lib/passport'; -import { - OAuthStartResponse, - SignInResolver, - AuthHandler, - AuthResolverContext, -} from '../types'; -import { - OAuthAdapter, - OAuthProviderOptions, - OAuthHandlers, - OAuthResponse, - OAuthEnvironmentHandler, - OAuthStartRequest, - OAuthRefreshRequest, - encodeState, - OAuthResult, -} from '../../lib/oauth'; +import { SignInResolver, AuthHandler } from '../types'; +import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; - -type PrivateInfo = { - refreshToken: string; -}; - -export type GitlabAuthProviderOptions = OAuthProviderOptions & { - baseUrl: string; - signInResolver?: SignInResolver; - authHandler: AuthHandler; - resolverContext: AuthResolverContext; -}; - -export const gitlabUsernameEntityNameSignInResolver: SignInResolver< - OAuthResult -> = async (info, ctx) => { - const { result } = info; - - const id = result.fullProfile.username; - if (!id) { - throw new Error(`GitLab user profile does not contain a username`); - } - - return ctx.signInWithCatalogUser({ entityRef: { name: id } }); -}; - -export const gitlabDefaultAuthHandler: AuthHandler = async ({ - fullProfile, - params, -}) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), -}); - -export class GitlabAuthProvider implements OAuthHandlers { - private readonly _strategy: GitlabStrategy; - private readonly signInResolver?: SignInResolver; - private readonly authHandler: AuthHandler; - private readonly resolverContext: AuthResolverContext; - - constructor(options: GitlabAuthProviderOptions) { - this.resolverContext = options.resolverContext; - this.authHandler = options.authHandler; - this.signInResolver = options.signInResolver; - - this._strategy = new GitlabStrategy( - { - clientID: options.clientId, - clientSecret: options.clientSecret, - callbackURL: options.callbackUrl, - baseURL: options.baseUrl, - authorizationURL: `${options.baseUrl}/oauth/authorize`, - tokenURL: `${options.baseUrl}/oauth/token`, - profileURL: `${options.baseUrl}/api/v4/user`, - }, - ( - accessToken: any, - refreshToken: any, - params: any, - fullProfile: any, - done: PassportDoneCallback, - ) => { - done( - undefined, - { fullProfile, params, accessToken }, - { - refreshToken, - }, - ); - }, - ); - } - - async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this._strategy, { - scope: req.scope, - state: encodeState(req.state), - }); - } - - async handler(req: express.Request) { - const { result, privateInfo } = await executeFrameHandlerStrategy< - OAuthResult, - PrivateInfo - >(req, this._strategy); - - return { - response: await this.handleResult(result), - refreshToken: privateInfo.refreshToken, - }; - } - - async refresh(req: OAuthRefreshRequest) { - const { accessToken, refreshToken, params } = - await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); - - const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, - accessToken, - ); - return { - response: await this.handleResult({ - fullProfile, - params, - accessToken, - }), - refreshToken, - }; - } - - private async handleResult(result: OAuthResult): Promise { - const { profile } = await this.authHandler(result, this.resolverContext); - - const response: OAuthResponse = { - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - profile, - }; - - if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( - { - result, - profile, - }, - this.resolverContext, - ); - } - - return response; - } -} +import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + adaptLegacyOAuthHandler, + adaptLegacyOAuthSignInResolver, +} from '../../lib/legacy'; +import { gitlabAuthenticator } from '@backstage/plugin-auth-backend-module-gitlab-provider'; /** * Auth provider integration for GitLab auth @@ -201,34 +44,10 @@ export const gitlab = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getOptionalString('audience'); - const baseUrl = audience || 'https://gitlab.com'; - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - - const authHandler: AuthHandler = - options?.authHandler ?? gitlabDefaultAuthHandler; - - const provider = new GitlabAuthProvider({ - clientId, - clientSecret, - callbackUrl, - baseUrl, - authHandler, - signInResolver: options?.signIn?.resolver, - resolverContext, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - providerId, - callbackUrl, - }); - }); + return createOAuthProviderFactory({ + authenticator: gitlabAuthenticator, + profileTransform: adaptLegacyOAuthHandler(options?.authHandler), + signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver), + }); }, }); diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index 3c0b367be2..abde2b66d1 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -14,74 +14,57 @@ * limitations under the License. */ -import { GoogleAuthProvider } from './provider'; -import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { OAuthResult } from '../../lib/oauth'; -import { AuthResolverContext } from '../types'; +import { googleAuthenticator } from '@backstage/plugin-auth-backend-module-google-provider'; +import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { google } from './provider'; -jest.mock('../../lib/passport/PassportStrategyHelper', () => { - return { - executeFrameHandlerStrategy: jest.fn(), - executeRefreshTokenStrategy: jest.fn(), - executeFetchUserProfileStrategy: jest.fn(), - }; -}); - -const mockFrameHandler = jest.spyOn( - helpers, - 'executeFrameHandlerStrategy', -) as unknown as jest.MockedFunction< - () => Promise<{ result: OAuthResult; privateInfo: any }> ->; +jest.mock('@backstage/plugin-auth-node', () => ({ + ...jest.requireActual('@backstage/plugin-auth-node'), + createOAuthProviderFactory: jest.fn(() => 'provider-factory'), +})); describe('createGoogleProvider', () => { - it('should auth', async () => { - const provider = new GoogleAuthProvider({ - resolverContext: {} as AuthResolverContext, - authHandler: async ({ fullProfile }) => ({ - profile: { - email: fullProfile.emails![0]!.value, - displayName: fullProfile.displayName, - picture: 'http://google.com/lols', - }, - }), - clientId: 'mock', - clientSecret: 'mock', - callbackUrl: 'mock', - }); + afterEach(() => jest.clearAllMocks()); - mockFrameHandler.mockResolvedValueOnce({ - result: { - fullProfile: { - emails: [{ value: 'conrad@example.com' }], - displayName: 'Conrad', - id: 'conrad', - provider: 'google', - }, - params: { - id_token: 'idToken', - scope: 'scope', - expires_in: 123, - }, - accessToken: 'accessToken', - }, - privateInfo: { - refreshToken: 'wacka', - }, + it('should be created', async () => { + expect(google.create()).toBe('provider-factory'); + + expect(createOAuthProviderFactory).toHaveBeenCalledWith({ + authenticator: googleAuthenticator, }); - const { response } = await provider.handler({} as any); - expect(response).toEqual({ - providerInfo: { - accessToken: 'accessToken', - expiresInSeconds: 123, - idToken: 'idToken', - scope: 'scope', - }, - profile: { - email: 'conrad@example.com', - displayName: 'Conrad', - picture: 'http://google.com/lols', - }, + }); + + it('should be created with sign-in resolver', async () => { + expect(google.create({ signIn: { resolver: jest.fn() } })).toBe( + 'provider-factory', + ); + + expect(createOAuthProviderFactory).toHaveBeenCalledWith({ + authenticator: googleAuthenticator, + signInResolver: expect.any(Function), + }); + }); + + it('should be created with sign-in resolver and auth handler', async () => { + expect( + google.create({ + signIn: { resolver: jest.fn() }, + authHandler: jest.fn(), + }), + ).toBe('provider-factory'); + + expect(createOAuthProviderFactory).toHaveBeenCalledWith({ + authenticator: googleAuthenticator, + signInResolver: expect.any(Function), + profileTransform: expect.any(Function), + }); + }); + + it('should have resolvers', () => { + expect(google.resolvers).toEqual({ + emailLocalPartMatchingUserEntityName: expect.any(Function), + emailMatchingUserEntityAnnotation: expect.any(Function), + emailMatchingUserEntityProfileEmail: expect.any(Function), }); }); }); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index afe5c27f6b..d467977094 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -14,166 +14,22 @@ * limitations under the License. */ -import express from 'express'; -import passport from 'passport'; -import { OAuth2Client } from 'google-auth-library'; -import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { - encodeState, - OAuthAdapter, - OAuthEnvironmentHandler, - OAuthHandlers, - OAuthProviderOptions, - OAuthRefreshRequest, - OAuthResponse, - OAuthResult, - OAuthStartRequest, - OAuthLogoutRequest, -} from '../../lib/oauth'; + googleAuthenticator, + googleSignInResolvers, +} from '@backstage/plugin-auth-backend-module-google-provider'; import { - executeFetchUserProfileStrategy, - executeFrameHandlerStrategy, - executeRedirectStrategy, - executeRefreshTokenStrategy, - makeProfileInfo, - PassportDoneCallback, -} from '../../lib/passport'; + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { - AuthHandler, - AuthResolverContext, - OAuthStartResponse, - SignInResolver, -} from '../types'; + adaptLegacyOAuthHandler, + adaptLegacyOAuthSignInResolver, + adaptOAuthSignInResolverToLegacy, +} from '../../lib/legacy'; +import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { - commonByEmailLocalPartResolver, - commonByEmailResolver, -} from '../resolvers'; - -type PrivateInfo = { - refreshToken: string; -}; - -type Options = OAuthProviderOptions & { - signInResolver?: SignInResolver; - authHandler: AuthHandler; - resolverContext: AuthResolverContext; -}; - -export class GoogleAuthProvider implements OAuthHandlers { - private readonly strategy: GoogleStrategy; - private readonly signInResolver?: SignInResolver; - private readonly authHandler: AuthHandler; - private readonly resolverContext: AuthResolverContext; - - constructor(options: Options) { - this.authHandler = options.authHandler; - this.signInResolver = options.signInResolver; - this.resolverContext = options.resolverContext; - this.strategy = new GoogleStrategy( - { - clientID: options.clientId, - clientSecret: options.clientSecret, - callbackURL: options.callbackUrl, - passReqToCallback: false, - }, - ( - accessToken: any, - refreshToken: any, - params: any, - fullProfile: passport.Profile, - done: PassportDoneCallback, - ) => { - done( - undefined, - { - fullProfile, - params, - accessToken, - refreshToken, - }, - { - refreshToken, - }, - ); - }, - ); - } - - async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this.strategy, { - accessType: 'offline', - prompt: 'consent', - scope: req.scope, - state: encodeState(req.state), - }); - } - - async handler(req: express.Request) { - const { result, privateInfo } = await executeFrameHandlerStrategy< - OAuthResult, - PrivateInfo - >(req, this.strategy); - - return { - response: await this.handleResult(result), - refreshToken: privateInfo.refreshToken, - }; - } - - async logout(req: OAuthLogoutRequest) { - const oauthClient = new OAuth2Client(); - await oauthClient.revokeToken(req.refreshToken); - } - - async refresh(req: OAuthRefreshRequest) { - const { accessToken, refreshToken, params } = - await executeRefreshTokenStrategy( - this.strategy, - req.refreshToken, - req.scope, - ); - const fullProfile = await executeFetchUserProfileStrategy( - this.strategy, - accessToken, - ); - - return { - response: await this.handleResult({ - fullProfile, - params, - accessToken, - }), - refreshToken, - }; - } - - private async handleResult(result: OAuthResult) { - const { profile } = await this.authHandler(result, this.resolverContext); - - const response: OAuthResponse = { - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - profile, - }; - - if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( - { - result, - profile, - }, - this.resolverContext, - ); - } - - return response; - } -} +import { AuthHandler, SignInResolver } from '../types'; /** * Auth provider integration for Google auth @@ -198,62 +54,18 @@ export const google = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); - - const provider = new GoogleAuthProvider({ - clientId, - clientSecret, - callbackUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - resolverContext, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - providerId, - callbackUrl, - }); - }); - }, - resolvers: { - /** - * Looks up the user by matching their email local part to the entity name. - */ - emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver, - /** - * Looks up the user by matching their email to the entity email. - */ - emailMatchingUserEntityProfileEmail: () => commonByEmailResolver, - /** - * Looks up the user by matching their email to the `google.com/email` annotation. - */ - emailMatchingUserEntityAnnotation(): SignInResolver { - return async (info, ctx) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Google profile contained no email'); - } - - return ctx.signInWithCatalogUser({ - annotations: { - 'google.com/email': profile.email, - }, - }); - }; - }, + return createOAuthProviderFactory({ + authenticator: googleAuthenticator, + profileTransform: adaptLegacyOAuthHandler(options?.authHandler), + signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver), + }); }, + resolvers: adaptOAuthSignInResolverToLegacy({ + emailLocalPartMatchingUserEntityName: + commonSignInResolvers.emailLocalPartMatchingUserEntityName(), + emailMatchingUserEntityProfileEmail: + commonSignInResolvers.emailMatchingUserEntityProfileEmail(), + emailMatchingUserEntityAnnotation: + googleSignInResolvers.emailMatchingUserEntityAnnotation(), + }), }); diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index 6f8df2d4df..7bbfd085d6 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -30,6 +30,10 @@ describe('MicrosoftAuthProvider', () => { const state = Buffer.from( `nonce=${encodeURIComponent(nonce)}&env=development`, ).toString('hex'); + const mockBackstageToken = `header.${Buffer.from( + JSON.stringify({ sub: 'user:default/mock' }), + 'utf8', + ).toString('base64')}.backstage`; const server = setupServer(); const microsoftApi = new FakeMicrosoftAPI(); @@ -45,6 +49,9 @@ describe('MicrosoftAuthProvider', () => { }, })({ providerId: 'microsoft', + baseUrl: 'http://backstage.test/api/auth', + appUrl: 'http://backstage.test', + isOriginAllowed: _ => true, globalConfig: { baseUrl: 'http://backstage.test/api/auth', appUrl: 'http://backstage.test', @@ -61,10 +68,9 @@ describe('MicrosoftAuthProvider', () => { resolverContext: { issueToken: jest.fn(), findCatalogUser: jest.fn(), - signInWithCatalogUser: _ => - Promise.resolve({ - token: 'header.e30K.backstage', - }), + signInWithCatalogUser: async _ => ({ + token: mockBackstageToken, + }), } as AuthResolverContext, }) as AuthProviderRouteHandlers; @@ -206,8 +212,12 @@ describe('MicrosoftAuthProvider', () => { displayName: 'Conrad', }, backstageIdentity: { - token: 'header.e30K.backstage', - identity: { type: 'user', ownershipEntityRefs: [] }, + token: mockBackstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, }, }, }), @@ -327,8 +337,12 @@ describe('MicrosoftAuthProvider', () => { displayName: 'Conrad', }, backstageIdentity: { - token: 'header.e30K.backstage', - identity: { type: 'user', ownershipEntityRefs: [] }, + token: mockBackstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, }, }, }), @@ -427,7 +441,7 @@ describe('MicrosoftAuthProvider', () => { expect(response.json).toHaveBeenCalledWith( expect.objectContaining({ backstageIdentity: expect.objectContaining({ - token: 'header.e30K.backstage', + token: mockBackstageToken, }), }), ); diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index bc581f6d10..8fc8459f10 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -47,7 +47,7 @@ import { commonByEmailLocalPartResolver, commonByEmailResolver, } from '../resolvers'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import fetch from 'node-fetch'; import { decodeJwt } from 'jose'; import { Profile as PassportProfile } from 'passport'; @@ -60,7 +60,7 @@ type PrivateInfo = { type Options = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; - logger: Logger; + logger: LoggerService; resolverContext: AuthResolverContext; authorizationUrl?: string; tokenUrl?: string; @@ -70,7 +70,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers { private readonly _strategy: MicrosoftStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly logger: Logger; + private readonly logger: LoggerService; private readonly resolverContext: AuthResolverContext; constructor(options: Options) { diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts index 0279604f2d..f585d4831f 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts @@ -22,7 +22,7 @@ jest.mock('@backstage/catalog-client'); import { AuthenticationError } from '@backstage/errors'; import express from 'express'; import * as jose from 'jose'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { AuthHandler, AuthResolverContext, SignInResolver } from '../types'; import { oauth2Proxy, @@ -36,7 +36,7 @@ describe('Oauth2ProxyAuthProvider', () => { 'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob'; let provider: Oauth2ProxyAuthProvider; - let logger: jest.Mocked; + let logger: jest.Mocked; let signInResolver: jest.MockedFunction< SignInResolver> >; @@ -53,7 +53,7 @@ describe('Oauth2ProxyAuthProvider', () => { >; authHandler = jest.fn(); signInResolver = jest.fn(); - logger = { error: jest.fn() } as unknown as jest.Mocked; + logger = { error: jest.fn() } as unknown as jest.Mocked; mockResponse = { status: jest.fn(), diff --git a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts index 761618c225..1fa8f4a2fa 100644 --- a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts +++ b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts @@ -14,34 +14,11 @@ * limitations under the License. */ -import { - BackstageIdentityResponse, - BackstageSignInResult, -} from '@backstage/plugin-auth-node'; - -function parseJwtPayload(token: string) { - const [_header, payload, _signature] = token.split('.'); - return JSON.parse(Buffer.from(payload, 'base64').toString()); -} +import { prepareBackstageIdentityResponse as _prepareBackstageIdentityResponse } from '@backstage/plugin-auth-node'; /** - * Parses a Backstage-issued token and decorates the - * {@link @backstage/plugin-auth-node#BackstageIdentityResponse} with identity information sourced from the - * token. - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export function prepareBackstageIdentityResponse( - result: BackstageSignInResult, -): BackstageIdentityResponse { - const { sub, ent } = parseJwtPayload(result.token); - - return { - ...result, - identity: { - type: 'user', - userEntityRef: sub, - ownershipEntityRefs: ent ?? [], - }, - }; -} +export const prepareBackstageIdentityResponse = + _prepareBackstageIdentityResponse; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 1459cff484..354387153c 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -14,126 +14,42 @@ * limitations under the License. */ -import { GetEntitiesRequest } from '@backstage/catalog-client'; -import { Entity } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; import { - BackstageIdentityResponse, - BackstageSignInResult, + AuthProviderConfig as _AuthProviderConfig, + AuthProviderRouteHandlers as _AuthProviderRouteHandlers, + AuthProviderFactory as _AuthProviderFactory, + AuthResolverCatalogUserQuery as _AuthResolverCatalogUserQuery, + AuthResolverContext as _AuthResolverContext, + ClientAuthResponse as _ClientAuthResponse, + CookieConfigurer as _CookieConfigurer, + ProfileInfo as _ProfileInfo, + SignInInfo as _SignInInfo, + SignInResolver as _SignInResolver, } from '@backstage/plugin-auth-node'; -import express from 'express'; -import { Logger } from 'winston'; -import { TokenParams } from '../identity/types'; import { OAuthStartRequest } from '../lib/oauth/types'; /** - * A query for a single user in the catalog. - * - * If `entityRef` is used, the default kind is `'User'`. - * - * If `annotations` are used, all annotations must be present and - * match the provided value exactly. Only entities of kind `'User'` will be considered. - * - * If `filter` are used they are passed on as they are to the `CatalogApi`. - * - * Regardless of the query method, the query must match exactly one entity - * in the catalog, or an error will be thrown. - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type AuthResolverCatalogUserQuery = - | { - entityRef: - | string - | { - kind?: string; - namespace?: string; - name: string; - }; - } - | { - annotations: Record; - } - | { - filter: Exclude; - }; +export type AuthResolverCatalogUserQuery = _AuthResolverCatalogUserQuery; /** - * The context that is used for auth processing. - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type AuthResolverContext = { - /** - * Issues a Backstage token using the provided parameters. - */ - issueToken(params: TokenParams): Promise<{ token: string }>; - - /** - * Finds a single user in the catalog using the provided query. - * - * See {@link AuthResolverCatalogUserQuery} for details. - */ - findCatalogUser( - query: AuthResolverCatalogUserQuery, - ): Promise<{ entity: Entity }>; - - /** - * Finds a single user in the catalog using the provided query, and then - * issues an identity for that user using default ownership resolution. - * - * See {@link AuthResolverCatalogUserQuery} for details. - */ - signInWithCatalogUser( - query: AuthResolverCatalogUserQuery, - ): Promise; -}; +export type AuthResolverContext = _AuthResolverContext; /** - * The callback used to resolve the cookie configuration for auth providers that use cookies. * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type CookieConfigurer = (ctx: { - /** ID of the auth provider that this configuration applies to */ - providerId: string; - /** The externally reachable base URL of the auth-backend plugin */ - baseUrl: string; - /** The configured callback URL of the auth provider */ - callbackUrl: string; - /** The origin URL of the app */ - appOrigin: string; -}) => { - domain: string; - path: string; - secure: boolean; - sameSite?: 'none' | 'lax' | 'strict'; -}; +export type CookieConfigurer = _CookieConfigurer; -/** @public */ -export type AuthProviderConfig = { - /** - * The protocol://domain[:port] where the app is hosted. This is used to construct the - * callbackURL to redirect to once the user signs in to the auth provider. - */ - baseUrl: string; - - /** - * The base URL of the app as provided by app.baseUrl - */ - appUrl: string; - - /** - * A function that is called to check whether an origin is allowed to receive the authentication result. - */ - isOriginAllowed: (origin: string) => boolean; - - /** - * The function used to resolve cookie configuration based on the auth provider options. - */ - cookieConfigurer?: CookieConfigurer; -}; - -/** @public */ +/** + * @public + * @deprecated Use `createOAuthAuthenticator` from `@backstage/plugin-auth-node` instead + */ export type OAuthStartResponse = { /** * URL to redirect to @@ -146,138 +62,53 @@ export type OAuthStartResponse = { }; /** - * Any Auth provider needs to implement this interface which handles the routes in the - * auth backend. Any auth API requests from the frontend reaches these methods. - * - * The routes in the auth backend API are tied to these methods like below - * - * `/auth/[provider]/start -> start` - * `/auth/[provider]/handler/frame -> frameHandler` - * `/auth/[provider]/refresh -> refresh` - * `/auth/[provider]/logout -> logout` - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export interface AuthProviderRouteHandlers { - /** - * Handles the start route of the API. This initiates a sign in request with an auth provider. - * - * Request - * - scopes for the auth request (Optional) - * Response - * - redirect to the auth provider for the user to sign in or consent. - * - sets a nonce cookie and also pass the nonce as 'state' query parameter in the redirect request - */ - start(req: express.Request, res: express.Response): Promise; - - /** - * Once the user signs in or consents in the OAuth screen, the auth provider redirects to the - * callbackURL which is handled by this method. - * - * Request - * - to contain a nonce cookie and a 'state' query parameter - * Response - * - postMessage to the window with a payload that contains accessToken, expiryInSeconds?, idToken? and scope. - * - sets a refresh token cookie if the auth provider supports refresh tokens - */ - frameHandler(req: express.Request, res: express.Response): Promise; - - /** - * (Optional) If the auth provider supports refresh tokens then this method handles - * requests to get a new access token. - * - * Request - * - to contain a refresh token cookie and scope (Optional) query parameter. - * Response - * - payload with accessToken, expiryInSeconds?, idToken?, scope and user profile information. - */ - refresh?(req: express.Request, res: express.Response): Promise; - - /** - * (Optional) Handles sign out requests - * - * Response - * - removes the refresh token cookie - */ - logout?(req: express.Request, res: express.Response): Promise; -} - -/** @public */ -export type AuthProviderFactory = (options: { - providerId: string; - globalConfig: AuthProviderConfig; - config: Config; - logger: Logger; - resolverContext: AuthResolverContext; -}) => AuthProviderRouteHandlers; - -/** @public */ -export type AuthResponse = { - providerInfo: ProviderInfo; - profile: ProfileInfo; - backstageIdentity?: BackstageIdentityResponse; -}; +export type AuthProviderConfig = _AuthProviderConfig; /** - * Used to display login information to user, i.e. sidebar popup. - * - * It is also temporarily used as the profile of the signed-in user's Backstage - * identity, but we want to replace that with data from identity and/org catalog - * service - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type ProfileInfo = { - /** - * Email ID of the signed in user. - */ - email?: string; - /** - * Display name that can be presented to the signed in user. - */ - displayName?: string; - /** - * URL to an image that can be used as the display image or avatar of the - * signed in user. - */ - picture?: string; -}; +export type AuthProviderRouteHandlers = _AuthProviderRouteHandlers; /** - * Type of sign in information context. Includes the profile information and - * authentication result which contains auth related information. - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type SignInInfo = { - /** - * The simple profile passed down for use in the frontend. - */ - profile: ProfileInfo; - - /** - * The authentication result that was received from the authentication - * provider. - */ - result: TAuthResult; -}; +export type AuthProviderFactory = _AuthProviderFactory; /** - * Describes the function which handles the result of a successful - * authentication. Must return a valid {@link @backstage/plugin-auth-node#BackstageSignInResult}. - * * @public + * @deprecated import `ClientAuthResponse` from `@backstage/plugin-auth-node` instead */ -export type SignInResolver = ( - info: SignInInfo, - context: AuthResolverContext, -) => Promise; +export type AuthResponse = _ClientAuthResponse; + +/** + * @public + * @deprecated import from `@backstage/plugin-auth-node` instead + */ +export type ProfileInfo = _ProfileInfo; + +/** + * @public + * @deprecated import from `@backstage/plugin-auth-node` instead + */ +export type SignInInfo = _SignInInfo; + +/** + * @public + * @deprecated import from `@backstage/plugin-auth-node` instead + */ +export type SignInResolver = _SignInResolver; /** * The return type of an authentication handler. Must contain valid profile * information. * * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead */ export type AuthHandlerResult = { profile: ProfileInfo }; @@ -293,13 +124,17 @@ export type AuthHandlerResult = { profile: ProfileInfo }; * group of users. * * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead */ export type AuthHandler = ( input: TAuthResult, context: AuthResolverContext, ) => Promise; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type StateEncoder = ( req: OAuthStartRequest, ) => Promise<{ encodedState: string }>; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index fa8dd95727..e3bfc1c3f2 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -17,7 +17,7 @@ import express from 'express'; import Router from 'express-promise-router'; import cookieParser from 'cookie-parser'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { defaultAuthProviderFactories, AuthProviderFactory, @@ -44,13 +44,14 @@ export type ProviderFactories = { [s: string]: AuthProviderFactory }; /** @public */ export interface RouterOptions { - logger: Logger; + logger: LoggerService; database: PluginDatabaseManager; config: Config; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; tokenFactoryAlgorithm?: string; providerFactories?: ProviderFactories; + disableDefaultProviderFactories?: boolean; catalogApi?: CatalogApi; } @@ -65,7 +66,7 @@ export async function createRouter( database, tokenManager, tokenFactoryAlgorithm, - providerFactories, + providerFactories = {}, catalogApi, } = options; const router = Router(); @@ -85,7 +86,9 @@ export async function createRouter( keyStore, keyDurationSeconds, logger: logger.child({ component: 'token-factory' }), - algorithm: tokenFactoryAlgorithm, + algorithm: + tokenFactoryAlgorithm ?? + config.getOptionalString('auth.identityTokenAlgorithm'), }); const secret = config.getOptionalString('auth.session.secret'); @@ -113,23 +116,28 @@ export async function createRouter( router.use(express.urlencoded({ extended: false })); router.use(express.json()); - const allProviderFactories = { - ...defaultAuthProviderFactories, - ...providerFactories, - }; - const providersConfig = config.getConfig('auth.providers'); - const configuredProviders = providersConfig.keys(); + const allProviderFactories = options.disableDefaultProviderFactories + ? providerFactories + : { + ...defaultAuthProviderFactories, + ...providerFactories, + }; + + const providersConfig = config.getOptionalConfig('auth.providers'); const isOriginAllowed = createOriginFilter(config); for (const [providerId, providerFactory] of Object.entries( allProviderFactories, )) { - if (configuredProviders.includes(providerId)) { + if (providersConfig?.has(providerId)) { logger.info(`Configuring auth provider: ${providerId}`); try { const provider = providerFactory({ providerId, + appUrl, + baseUrl: authUrl, + isOriginAllowed, globalConfig: { baseUrl: authUrl, appUrl, diff --git a/plugins/auth-backend/src/service/standaloneServer.ts b/plugins/auth-backend/src/service/standaloneServer.ts index 16ebbc9345..abda3a341c 100644 --- a/plugins/auth-backend/src/service/standaloneServer.ts +++ b/plugins/auth-backend/src/service/standaloneServer.ts @@ -23,11 +23,11 @@ import { } from '@backstage/backend-common'; import { Server } from 'http'; import Knex from 'knex'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { createRouter } from './router'; export interface ServerOptions { - logger: Logger; + logger: LoggerService; } export async function startStandaloneServer( diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index 30c133338b..d92de891de 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-auth-node +## 0.3.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: Introduced a new system for building auth providers for `@backstage/plugin-auth-backend`, which both increases the amount of code re-use across providers, and also works better with the new backend system. + + Many existing types have been moved from `@backstage/plugin-auth-backend` in order to avoid a direct dependency on the plugin from modules. + + Auth provider integrations are now primarily implemented through a pattern of creating "authenticators", which are in turn specific to each kind of integrations. Initially there are two types: `createOAuthAuthenticator` and `createProxyAuthenticator`. These come paired with functions that let you create the corresponding route handlers, `createOAuthRouteHandlers` and `createProxyAuthRouteHandlers`, as well as provider factories, `createOAuthProviderFactory` and `createProxyAuthProviderFactory`. This new authenticator pattern allows the sign-in logic to be separated from the auth integration logic, allowing it to be completely re-used across all providers of the same kind. + + The new provider factories also implement a new declarative way to configure sign-in resolvers, rather than configuration through code. Sign-in resolvers can now be configured through the `resolvers` configuration key, where the first resolver that provides an identity will be used, for example: + + ```yaml + auth: + providers: + google: + development: + clientId: ... + clientSecret: ... + signIn: + resolvers: + - resolver: emailMatchingUserEntityAnnotation + - resolver: emailLocalPartMatchingUserEntityName + ``` + + These configurable resolvers are created with a new `createSignInResolverFactory` function, which creates a sign-in resolver factory, optionally with an options schema that will be used both when configuring the sign-in resolver through configuration and code. + + The internal helpers from `@backstage/plugin-auth-backend` that were used to implement auth providers using passport strategies have now also been made available as public API, through `PassportHelpers` and `PassportOAuthAuthenticatorHelper`. + +### Patch Changes + +- 18619f793c94: The `BackstageIdentityResponse` interface now has an optional `expiresInSeconds` field that can be used to signal session expiration. The `prepareBackstageIdentityResponse` utility will now also read the expiration from the provided token, and include it in the response. +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + ## 0.2.17 ### Patch Changes diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md index bc9e392929..a782847d7a 100644 --- a/plugins/auth-node/api-report.md +++ b/plugins/auth-node/api-report.md @@ -3,11 +3,104 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackstageIdentityResponse as BackstageIdentityResponse_2 } from '@backstage/plugin-auth-node'; +import { BackstageSignInResult as BackstageSignInResult_2 } from '@backstage/plugin-auth-node'; +import { Config } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; +import { EntityFilterQuery } from '@backstage/catalog-client'; +import express from 'express'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Profile } from 'passport'; import { Request as Request_2 } from 'express'; +import { Response as Response_2 } from 'express'; +import { Strategy } from 'passport'; +import { ZodSchema } from 'zod'; +import { ZodTypeDef } from 'zod'; + +// @public @deprecated (undocumented) +export type AuthProviderConfig = { + baseUrl: string; + appUrl: string; + isOriginAllowed: (origin: string) => boolean; + cookieConfigurer?: CookieConfigurer; +}; + +// @public (undocumented) +export type AuthProviderFactory = (options: { + providerId: string; + globalConfig: AuthProviderConfig; + config: Config; + logger: LoggerService; + resolverContext: AuthResolverContext; + baseUrl: string; + appUrl: string; + isOriginAllowed: (origin: string) => boolean; + cookieConfigurer?: CookieConfigurer; +}) => AuthProviderRouteHandlers; + +// @public (undocumented) +export interface AuthProviderRegistrationOptions { + // (undocumented) + factory: AuthProviderFactory; + // (undocumented) + providerId: string; +} + +// @public +export interface AuthProviderRouteHandlers { + frameHandler(req: Request_2, res: Response_2): Promise; + logout?(req: Request_2, res: Response_2): Promise; + refresh?(req: Request_2, res: Response_2): Promise; + start(req: Request_2, res: Response_2): Promise; +} + +// @public (undocumented) +export interface AuthProvidersExtensionPoint { + // (undocumented) + registerProvider(options: AuthProviderRegistrationOptions): void; +} + +// @public (undocumented) +export const authProvidersExtensionPoint: ExtensionPoint; + +// @public +export type AuthResolverCatalogUserQuery = + | { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + } + | { + annotations: Record; + } + | { + filter: EntityFilterQuery; + }; + +// @public +export type AuthResolverContext = { + issueToken(params: TokenParams): Promise<{ + token: string; + }>; + findCatalogUser(query: AuthResolverCatalogUserQuery): Promise<{ + entity: Entity; + }>; + signInWithCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise; +}; // @public export interface BackstageIdentityResponse extends BackstageSignInResult { + expiresInSeconds?: number; identity: BackstageUserIdentity; } @@ -23,6 +116,99 @@ export type BackstageUserIdentity = { ownershipEntityRefs: string[]; }; +// @public (undocumented) +export type ClientAuthResponse = { + providerInfo: TProviderInfo; + profile: ProfileInfo; + backstageIdentity?: BackstageIdentityResponse; +}; + +// @public +export namespace commonSignInResolvers { + const emailMatchingUserEntityProfileEmail: SignInResolverFactory< + unknown, + unknown + >; + const emailLocalPartMatchingUserEntityName: SignInResolverFactory< + unknown, + unknown + >; +} + +// @public +export type CookieConfigurer = (ctx: { + providerId: string; + baseUrl: string; + callbackUrl: string; + appOrigin: string; +}) => { + domain: string; + path: string; + secure: boolean; + sameSite?: 'none' | 'lax' | 'strict'; +}; + +// @public (undocumented) +export function createOAuthAuthenticator( + authenticator: OAuthAuthenticator, +): OAuthAuthenticator; + +// @public (undocumented) +export function createOAuthProviderFactory(options: { + authenticator: OAuthAuthenticator; + stateTransform?: OAuthStateTransform; + profileTransform?: ProfileTransform>; + signInResolver?: SignInResolver>; + signInResolverFactories?: { + [name in string]: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; + }; +}): AuthProviderFactory; + +// @public (undocumented) +export function createOAuthRouteHandlers( + options: OAuthRouteHandlersOptions, +): AuthProviderRouteHandlers; + +// @public (undocumented) +export function createProxyAuthenticator( + authenticator: ProxyAuthenticator, +): ProxyAuthenticator; + +// @public (undocumented) +export function createProxyAuthProviderFactory(options: { + authenticator: ProxyAuthenticator; + profileTransform?: ProfileTransform; + signInResolver?: SignInResolver; + signInResolverFactories?: Record< + string, + SignInResolverFactory + >; +}): AuthProviderFactory; + +// @public (undocumented) +export function createProxyAuthRouteHandlers( + options: ProxyAuthRouteHandlersOptions, +): AuthProviderRouteHandlers; + +// @public (undocumented) +export function createSignInResolverFactory< + TAuthResult, + TOptionsOutput, + TOptionsInput, +>( + options: SignInResolverFactoryOptions< + TAuthResult, + TOptionsOutput, + TOptionsInput + >, +): SignInResolverFactory; + +// @public (undocumented) +export function decodeOAuthState(encodedState: string): OAuthState; + // @public export class DefaultIdentityClient implements IdentityApi { // @deprecated @@ -34,6 +220,9 @@ export class DefaultIdentityClient implements IdentityApi { ): Promise; } +// @public (undocumented) +export function encodeOAuthState(state: OAuthState): string; + // @public export function getBearerTokenFromAuthorizationHeader( authorizationHeader: unknown, @@ -65,4 +254,393 @@ export type IdentityClientOptions = { issuer?: string; algorithms?: string[]; }; + +// @public (undocumented) +export interface OAuthAuthenticator { + // (undocumented) + authenticate( + input: OAuthAuthenticatorAuthenticateInput, + ctx: TContext, + ): Promise>; + // (undocumented) + defaultProfileTransform: ProfileTransform>; + // (undocumented) + initialize(ctx: { callbackUrl: string; config: Config }): TContext; + // (undocumented) + logout?(input: OAuthAuthenticatorLogoutInput, ctx: TContext): Promise; + // (undocumented) + refresh( + input: OAuthAuthenticatorRefreshInput, + ctx: TContext, + ): Promise>; + // (undocumented) + shouldPersistScopes?: boolean; + // (undocumented) + start( + input: OAuthAuthenticatorStartInput, + ctx: TContext, + ): Promise<{ + url: string; + status?: number; + }>; +} + +// @public (undocumented) +export interface OAuthAuthenticatorAuthenticateInput { + // (undocumented) + req: Request_2; +} + +// @public (undocumented) +export interface OAuthAuthenticatorLogoutInput { + // (undocumented) + accessToken?: string; + // (undocumented) + refreshToken?: string; + // (undocumented) + req: Request_2; +} + +// @public (undocumented) +export interface OAuthAuthenticatorRefreshInput { + // (undocumented) + refreshToken: string; + // (undocumented) + req: Request_2; + // (undocumented) + scope: string; +} + +// @public (undocumented) +export interface OAuthAuthenticatorResult { + // (undocumented) + fullProfile: TProfile; + // (undocumented) + session: OAuthSession; +} + +// @public (undocumented) +export interface OAuthAuthenticatorStartInput { + // (undocumented) + req: Request_2; + // (undocumented) + scope: string; + // (undocumented) + state: string; +} + +// @public (undocumented) +export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { + constructor(handlers: Map); + // (undocumented) + frameHandler(req: express.Request, res: express.Response): Promise; + // (undocumented) + logout(req: express.Request, res: express.Response): Promise; + // (undocumented) + static mapConfig( + config: Config, + factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, + ): OAuthEnvironmentHandler; + // (undocumented) + refresh(req: express.Request, res: express.Response): Promise; + // (undocumented) + start(req: express.Request, res: express.Response): Promise; +} + +// @public (undocumented) +export interface OAuthRouteHandlersOptions { + // (undocumented) + appUrl: string; + // (undocumented) + authenticator: OAuthAuthenticator; + // (undocumented) + baseUrl: string; + // (undocumented) + config: Config; + // (undocumented) + cookieConfigurer?: CookieConfigurer; + // (undocumented) + isOriginAllowed: (origin: string) => boolean; + // (undocumented) + profileTransform?: ProfileTransform>; + // (undocumented) + providerId: string; + // (undocumented) + resolverContext: AuthResolverContext; + // (undocumented) + signInResolver?: SignInResolver>; + // (undocumented) + stateTransform?: OAuthStateTransform; +} + +// @public (undocumented) +export interface OAuthSession { + // (undocumented) + accessToken: string; + // (undocumented) + expiresInSeconds?: number; + // (undocumented) + idToken?: string; + // (undocumented) + refreshToken?: string; + // (undocumented) + refreshTokenExpiresInSeconds?: number; + // (undocumented) + scope: string; + // (undocumented) + tokenType: string; +} + +// @public +export type OAuthState = { + nonce: string; + env: string; + origin?: string; + scope?: string; + redirectUrl?: string; + flow?: string; +}; + +// @public (undocumented) +export type OAuthStateTransform = ( + state: OAuthState, + context: { + req: Request_2; + }, +) => Promise<{ + state: OAuthState; +}>; + +// @public (undocumented) +export type PassportDoneCallback = ( + err?: Error, + result?: TResult, + privateInfo?: TPrivateInfo, +) => void; + +// @public (undocumented) +export class PassportHelpers { + // (undocumented) + static executeFetchUserProfileStrategy( + providerStrategy: Strategy, + accessToken: string, + ): Promise; + // (undocumented) + static executeFrameHandlerStrategy( + req: Request_2, + providerStrategy: Strategy, + options?: Record, + ): Promise<{ + result: TResult; + privateInfo: TPrivateInfo; + }>; + // (undocumented) + static executeRedirectStrategy( + req: Request_2, + providerStrategy: Strategy, + options: Record, + ): Promise<{ + url: string; + status?: number; + }>; + // (undocumented) + static executeRefreshTokenStrategy( + providerStrategy: Strategy, + refreshToken: string, + scope: string, + ): Promise<{ + accessToken: string; + refreshToken?: string; + params: any; + }>; + // (undocumented) + static transformProfile: ( + profile: PassportProfile, + idToken?: string, + ) => ProfileInfo; +} + +// @public (undocumented) +export class PassportOAuthAuthenticatorHelper { + // (undocumented) + authenticate( + input: OAuthAuthenticatorAuthenticateInput, + ): Promise>; + // (undocumented) + static defaultProfileTransform: ProfileTransform< + OAuthAuthenticatorResult + >; + // (undocumented) + fetchProfile(accessToken: string): Promise; + // (undocumented) + static from(strategy: Strategy): PassportOAuthAuthenticatorHelper; + // (undocumented) + refresh( + input: OAuthAuthenticatorRefreshInput, + ): Promise>; + // (undocumented) + start( + input: OAuthAuthenticatorStartInput, + options: Record, + ): Promise<{ + url: string; + status?: number; + }>; +} + +// @public (undocumented) +export type PassportOAuthDoneCallback = PassportDoneCallback< + PassportOAuthResult, + PassportOAuthPrivateInfo +>; + +// @public (undocumented) +export type PassportOAuthPrivateInfo = { + refreshToken?: string; +}; + +// @public (undocumented) +export type PassportOAuthResult = { + fullProfile: PassportProfile; + params: { + id_token?: string; + scope: string; + token_type?: string; + expires_in: number; + }; + accessToken: string; +}; + +// @public (undocumented) +export type PassportProfile = Profile & { + avatarUrl?: string; +}; + +// @public +export function prepareBackstageIdentityResponse( + result: BackstageSignInResult_2, +): BackstageIdentityResponse_2; + +// @public +export type ProfileInfo = { + email?: string; + displayName?: string; + picture?: string; +}; + +// @public +export type ProfileTransform = ( + result: TResult, + context: AuthResolverContext, +) => Promise<{ + profile: ProfileInfo; +}>; + +// @public (undocumented) +export interface ProxyAuthenticator { + // (undocumented) + authenticate( + options: { + req: Request_2; + }, + ctx: TContext, + ): Promise<{ + result: TResult; + }>; + // (undocumented) + defaultProfileTransform: ProfileTransform; + // (undocumented) + initialize(ctx: { config: Config }): Promise; +} + +// @public (undocumented) +export interface ProxyAuthRouteHandlersOptions { + // (undocumented) + authenticator: ProxyAuthenticator; + // (undocumented) + config: Config; + // (undocumented) + profileTransform?: ProfileTransform; + // (undocumented) + resolverContext: AuthResolverContext; + // (undocumented) + signInResolver: SignInResolver; +} + +// @public (undocumented) +export function readDeclarativeSignInResolver( + options: ReadDeclarativeSignInResolverOptions, +): SignInResolver | undefined; + +// @public (undocumented) +export interface ReadDeclarativeSignInResolverOptions { + // (undocumented) + config: Config; + // (undocumented) + signInResolverFactories: { + [name in string]: SignInResolverFactory; + }; +} + +// @public (undocumented) +export function sendWebMessageResponse( + res: Response_2, + appOrigin: string, + response: WebMessageResponse, +): void; + +// @public +export type SignInInfo = { + profile: ProfileInfo; + result: TAuthResult; +}; + +// @public +export type SignInResolver = ( + info: SignInInfo, + context: AuthResolverContext, +) => Promise; + +// @public (undocumented) +export interface SignInResolverFactory { + // (undocumented) + ( + ...options: undefined extends TOptions + ? [options?: TOptions] + : [options: TOptions] + ): SignInResolver; + // (undocumented) + optionsJsonSchema?: JsonObject; +} + +// @public (undocumented) +export interface SignInResolverFactoryOptions< + TAuthResult, + TOptionsOutput, + TOptionsInput, +> { + // (undocumented) + create(options: TOptionsOutput): SignInResolver; + // (undocumented) + optionsSchema?: ZodSchema; +} + +// @public +export type TokenParams = { + claims: { + sub: string; + ent?: string[]; + } & Record; +}; + +// @public +export type WebMessageResponse = + | { + type: 'authorization_response'; + response: ClientAuthResponse; + } + | { + type: 'authorization_response'; + error: Error; + }; ``` diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index b5c402367b..ce2f3531c5 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.2.17", + "version": "0.3.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,19 +29,31 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/types": "workspace:^", "@types/express": "*", + "@types/passport": "^1.0.3", "express": "^4.17.1", "jose": "^4.6.0", + "lodash": "^4.17.21", "node-fetch": "^2.6.7", - "winston": "^3.2.1" + "passport": "^0.6.0", + "winston": "^3.2.1", + "zod": "^3.21.4", + "zod-to-json-schema": "^3.21.4" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "cookie-parser": "^1.4.6", + "express-promise-router": "^4.1.1", "lodash": "^4.17.21", "msw": "^1.0.0", + "supertest": "^6.1.3", "uuid": "^8.0.0" }, "files": [ diff --git a/plugins/auth-node/src/extensions/AuthProvidersExtensionPoint.ts b/plugins/auth-node/src/extensions/AuthProvidersExtensionPoint.ts new file mode 100644 index 0000000000..b67d5be017 --- /dev/null +++ b/plugins/auth-node/src/extensions/AuthProvidersExtensionPoint.ts @@ -0,0 +1,35 @@ +/* + * 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 { createExtensionPoint } from '@backstage/backend-plugin-api'; +import { AuthProviderFactory } from '../types'; + +/** @public */ +export interface AuthProviderRegistrationOptions { + providerId: string; + factory: AuthProviderFactory; +} + +/** @public */ +export interface AuthProvidersExtensionPoint { + registerProvider(options: AuthProviderRegistrationOptions): void; +} + +/** @public */ +export const authProvidersExtensionPoint = + createExtensionPoint({ + id: 'auth.providers', + }); diff --git a/plugins/auth-node/src/extensions/index.ts b/plugins/auth-node/src/extensions/index.ts new file mode 100644 index 0000000000..43e1751685 --- /dev/null +++ b/plugins/auth-node/src/extensions/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + authProvidersExtensionPoint, + type AuthProviderRegistrationOptions, + type AuthProvidersExtensionPoint, +} from './AuthProvidersExtensionPoint'; diff --git a/plugins/auth-node/src/flow/__testUtils__/parseWebMessageResponse.ts b/plugins/auth-node/src/flow/__testUtils__/parseWebMessageResponse.ts new file mode 100644 index 0000000000..eda8469ac6 --- /dev/null +++ b/plugins/auth-node/src/flow/__testUtils__/parseWebMessageResponse.ts @@ -0,0 +1,28 @@ +/* + * 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 { WebMessageResponse } from '../sendWebMessageResponse'; + +export function parseWebMessageResponse(text: string): { + response: WebMessageResponse; + origin: string; +} { + const [response, origin] = text.matchAll(/decodeURIComponent\('(.+?)'\)/g); + return { + response: JSON.parse(decodeURIComponent(response[1])), + origin: decodeURIComponent(origin[1]), + }; +} diff --git a/plugins/auth-node/src/flow/index.ts b/plugins/auth-node/src/flow/index.ts new file mode 100644 index 0000000000..51d91e5d58 --- /dev/null +++ b/plugins/auth-node/src/flow/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { + sendWebMessageResponse, + type WebMessageResponse, +} from './sendWebMessageResponse'; diff --git a/plugins/auth-node/src/flow/sendWebMessageResponse.test.ts b/plugins/auth-node/src/flow/sendWebMessageResponse.test.ts new file mode 100644 index 0000000000..cc192638f2 --- /dev/null +++ b/plugins/auth-node/src/flow/sendWebMessageResponse.test.ts @@ -0,0 +1,194 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { + safelyEncodeURIComponent, + sendWebMessageResponse, + type WebMessageResponse, +} from './sendWebMessageResponse'; +import { parseWebMessageResponse } from './__testUtils__/parseWebMessageResponse'; + +describe('oauth helpers', () => { + describe('safelyEncodeURIComponent', () => { + it('encodes all occurrences of single quotes', () => { + expect(safelyEncodeURIComponent("a'ö'b")).toBe('a%27%C3%B6%27b'); + }); + }); + + describe('sendWebMessageResponse', () => { + const appOrigin = 'http://localhost:3000'; + it('should post a message back with payload success', () => { + const mockResponse = { + end: jest.fn().mockReturnThis(), + setHeader: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + const data: WebMessageResponse = { + type: 'authorization_response', + response: { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + }, + backstageIdentity: { + token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, + }, + }, + }; + const encoded = safelyEncodeURIComponent(JSON.stringify(data)); + + sendWebMessageResponse(mockResponse, appOrigin, data); + expect(mockResponse.setHeader).toHaveBeenCalledTimes(3); + expect(mockResponse.end).toHaveBeenCalledTimes(1); + expect(mockResponse.end).toHaveBeenCalledWith( + expect.stringContaining(encoded), + ); + }); + + it('should post a message back with payload error', () => { + const mockResponse = { + end: jest.fn().mockReturnThis(), + setHeader: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + const data: WebMessageResponse = { + type: 'authorization_response', + error: new Error('Unknown error occurred'), + }; + const encoded = safelyEncodeURIComponent( + JSON.stringify({ + type: 'authorization_response', + error: { name: 'Error', message: 'Unknown error occurred' }, + }), + ); + + sendWebMessageResponse(mockResponse, appOrigin, data); + expect(mockResponse.setHeader).toHaveBeenCalledTimes(3); + expect(mockResponse.end).toHaveBeenCalledTimes(1); + expect(mockResponse.end).toHaveBeenCalledWith( + expect.stringContaining(encoded), + ); + }); + + it('should call postMessage twice but only one of them with target *', () => { + let responseBody = ''; + + const mockResponse = { + end: jest.fn(body => { + responseBody = body; + return this; + }), + setHeader: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + const data: WebMessageResponse = { + type: 'authorization_response', + response: { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + }, + backstageIdentity: { + token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, + }, + }, + }; + sendWebMessageResponse(mockResponse, appOrigin, data); + expect(parseWebMessageResponse(responseBody).response).toEqual({ + type: 'authorization_response', + response: { + providerInfo: expect.any(Object), + profile: { + email: 'foo@bar.com', + }, + backstageIdentity: expect.any(Object), + }, + }); + + const errData: WebMessageResponse = { + type: 'authorization_response', + error: new Error('Unknown error occurred'), + }; + sendWebMessageResponse(mockResponse, appOrigin, errData); + expect(parseWebMessageResponse(responseBody).response).toEqual({ + type: 'authorization_response', + error: { + name: 'Error', + message: 'Unknown error occurred', + }, + }); + }); + + it('handles single quotes and unicode chars safely', () => { + const mockResponse = { + end: jest.fn().mockReturnThis(), + setHeader: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + const data: WebMessageResponse = { + type: 'authorization_response', + response: { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + displayName: "Adam l'Hôpital", + }, + backstageIdentity: { + token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, + }, + }, + }; + + sendWebMessageResponse(mockResponse, appOrigin, data); + expect(mockResponse.setHeader).toHaveBeenCalledTimes(3); + expect(mockResponse.end).toHaveBeenCalledTimes(1); + expect(mockResponse.end).toHaveBeenCalledWith( + expect.stringContaining('Adam%20l%27H%C3%B4pital'), + ); + }); + }); +}); diff --git a/plugins/auth-node/src/flow/sendWebMessageResponse.ts b/plugins/auth-node/src/flow/sendWebMessageResponse.ts new file mode 100644 index 0000000000..4c7a85854d --- /dev/null +++ b/plugins/auth-node/src/flow/sendWebMessageResponse.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Response } from 'express'; +import crypto from 'crypto'; +import { ClientAuthResponse } from '../types'; +import { serializeError } from '@backstage/errors'; + +/** + * Payload sent as a post message after the auth request is complete. + * If successful then has a valid payload with Auth information else contains an error. + * + * @public + */ +export type WebMessageResponse = + | { + type: 'authorization_response'; + response: ClientAuthResponse; + } + | { + type: 'authorization_response'; + error: Error; + }; + +/** @internal */ +export function safelyEncodeURIComponent(value: string): string { + // Note the g at the end of the regex; all occurrences of single quotes must + // be replaced, which encodeURIComponent does not do itself by default + return encodeURIComponent(value).replace(/'/g, '%27'); +} + +/** @public */ +export function sendWebMessageResponse( + res: Response, + appOrigin: string, + response: WebMessageResponse, +): void { + const jsonData = JSON.stringify(response, (_, value) => { + if (value instanceof Error) { + return serializeError(value); + } + return value; + }); + const base64Data = safelyEncodeURIComponent(jsonData); + const base64Origin = safelyEncodeURIComponent(appOrigin); + + // NOTE: It is absolutely imperative that we use the safe encoder above, to + // be sure that the js code below does not allow the injection of malicious + // data. + + // TODO: Make target app origin configurable globally + + // + // postMessage fails silently if the targetOrigin is disallowed. + // So 2 postMessages are sent from the popup to the parent window. + // First, the origin being used to post the actual authorization response is + // shared with the parent window with a postMessage with targetOrigin '*'. + // Second, the actual authorization response is sent with the app origin + // as the targetOrigin. + // If the first message was received but the actual auth response was + // never received, the event listener can conclude that targetOrigin + // was disallowed, indicating potential misconfiguration. + // + const script = ` + var authResponse = decodeURIComponent('${base64Data}'); + var origin = decodeURIComponent('${base64Origin}'); + var originInfo = {'type': 'config_info', 'targetOrigin': origin}; + (window.opener || window.parent).postMessage(originInfo, '*'); + (window.opener || window.parent).postMessage(JSON.parse(authResponse), origin); + setTimeout(() => { + window.close(); + }, 100); // same as the interval of the core-app-api lib/loginPopup.ts (to address race conditions) + `; + const hash = crypto.createHash('sha256').update(script).digest('base64'); + + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Frame-Options', 'sameorigin'); + res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`); + res.end(``); +} diff --git a/plugins/auth-node/src/DefaultIdentityClient.test.ts b/plugins/auth-node/src/identity/DefaultIdentityClient.test.ts similarity index 99% rename from plugins/auth-node/src/DefaultIdentityClient.test.ts rename to plugins/auth-node/src/identity/DefaultIdentityClient.test.ts index 13d9c5a17b..b9bf3cabd2 100644 --- a/plugins/auth-node/src/DefaultIdentityClient.test.ts +++ b/plugins/auth-node/src/identity/DefaultIdentityClient.test.ts @@ -26,7 +26,7 @@ import { setupServer } from 'msw/node'; import { v4 as uuid } from 'uuid'; import { DefaultIdentityClient } from './DefaultIdentityClient'; -import { IdentityApiGetIdentityRequest } from './types'; +import { IdentityApiGetIdentityRequest } from './IdentityApi'; interface AnyJWK extends Record { use: 'sig'; diff --git a/plugins/auth-node/src/DefaultIdentityClient.ts b/plugins/auth-node/src/identity/DefaultIdentityClient.ts similarity index 97% rename from plugins/auth-node/src/DefaultIdentityClient.ts rename to plugins/auth-node/src/identity/DefaultIdentityClient.ts index 8af8bf09e1..1095609dec 100644 --- a/plugins/auth-node/src/DefaultIdentityClient.ts +++ b/plugins/auth-node/src/identity/DefaultIdentityClient.ts @@ -25,12 +25,9 @@ import { jwtVerify, } from 'jose'; import { GetKeyFunction } from 'jose/dist/types/types'; -import { - BackstageIdentityResponse, - IdentityApiGetIdentityRequest, -} from './types'; import { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; -import { IdentityApi } from './IdentityApi'; +import { IdentityApi, IdentityApiGetIdentityRequest } from './IdentityApi'; +import { BackstageIdentityResponse } from '../types'; const CLOCK_MARGIN_S = 10; diff --git a/plugins/auth-node/src/IdentityApi.ts b/plugins/auth-node/src/identity/IdentityApi.ts similarity index 80% rename from plugins/auth-node/src/IdentityApi.ts rename to plugins/auth-node/src/identity/IdentityApi.ts index 86171fa3ab..8322b9f827 100644 --- a/plugins/auth-node/src/IdentityApi.ts +++ b/plugins/auth-node/src/identity/IdentityApi.ts @@ -13,10 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - BackstageIdentityResponse, - IdentityApiGetIdentityRequest, -} from './types'; + +import { Request } from 'express'; +import { BackstageIdentityResponse } from '../types'; + +/** + * Options to request the identity from a Backstage backend request + * + * @public + */ +export type IdentityApiGetIdentityRequest = { + request: Request; +}; /** * An identity client api to authenticate Backstage diff --git a/plugins/auth-node/src/IdentityClient.test.ts b/plugins/auth-node/src/identity/IdentityClient.test.ts similarity index 100% rename from plugins/auth-node/src/IdentityClient.test.ts rename to plugins/auth-node/src/identity/IdentityClient.test.ts diff --git a/plugins/auth-node/src/IdentityClient.ts b/plugins/auth-node/src/identity/IdentityClient.ts similarity index 97% rename from plugins/auth-node/src/IdentityClient.ts rename to plugins/auth-node/src/identity/IdentityClient.ts index 8252a61f81..fd6580dbb2 100644 --- a/plugins/auth-node/src/IdentityClient.ts +++ b/plugins/auth-node/src/identity/IdentityClient.ts @@ -18,7 +18,7 @@ import { DefaultIdentityClient, IdentityClientOptions, } from './DefaultIdentityClient'; -import { BackstageIdentityResponse } from './types'; +import { BackstageIdentityResponse } from '../types'; /** * An identity client to interact with auth-backend and authenticate Backstage diff --git a/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts b/plugins/auth-node/src/identity/getBearerTokenFromAuthorizationHeader.test.ts similarity index 100% rename from plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts rename to plugins/auth-node/src/identity/getBearerTokenFromAuthorizationHeader.test.ts diff --git a/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts b/plugins/auth-node/src/identity/getBearerTokenFromAuthorizationHeader.ts similarity index 100% rename from plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts rename to plugins/auth-node/src/identity/getBearerTokenFromAuthorizationHeader.ts diff --git a/plugins/auth-node/src/identity/index.ts b/plugins/auth-node/src/identity/index.ts new file mode 100644 index 0000000000..24e2dcb691 --- /dev/null +++ b/plugins/auth-node/src/identity/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse'; +export { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; +export { + DefaultIdentityClient, + type IdentityClientOptions, +} from './DefaultIdentityClient'; +export { IdentityClient } from './IdentityClient'; +export type { IdentityApi, IdentityApiGetIdentityRequest } from './IdentityApi'; diff --git a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.test.ts b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.test.ts similarity index 53% rename from plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.test.ts rename to plugins/auth-node/src/identity/prepareBackstageIdentityResponse.test.ts index dac06f964e..d37224a11d 100644 --- a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.test.ts +++ b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.test.ts @@ -23,7 +23,37 @@ function mkToken(payload: unknown) { } describe('prepareBackstageIdentityResponse', () => { + afterEach(jest.resetAllMocks); + it('parses a complete token to determine the identity', () => { + jest.spyOn(Date, 'now').mockReturnValue(5000); + + const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'], exp: 1005 }); + expect( + prepareBackstageIdentityResponse({ + token, + }), + ).toEqual({ + token, + expiresInSeconds: 1000, + identity: { + type: 'user', + userEntityRef: 'k:ns/n', + ownershipEntityRefs: ['k:ns/o'], + }, + }); + }); + + it('should reject tokens without subject', () => { + const token = mkToken({}); + expect(() => + prepareBackstageIdentityResponse({ + token, + }), + ).toThrow('Identity response must return a token with subject claim'); + }); + + it('should treat expiration as optional', () => { const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'] }); expect( prepareBackstageIdentityResponse({ @@ -38,4 +68,15 @@ describe('prepareBackstageIdentityResponse', () => { }, }); }); + + it('should reject tokens with negative expiration', () => { + jest.spyOn(Date, 'now').mockReturnValue(5000); + + const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'], exp: 1 }); + expect(() => + prepareBackstageIdentityResponse({ + token, + }), + ).toThrow('Identity response must not return an expired token'); + }); }); diff --git a/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.ts b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.ts new file mode 100644 index 0000000000..33e9bafd0d --- /dev/null +++ b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { + BackstageIdentityResponse, + BackstageSignInResult, +} from '@backstage/plugin-auth-node'; + +function parseJwtPayload(token: string) { + const [_header, payload, _signature] = token.split('.'); + return JSON.parse(Buffer.from(payload, 'base64').toString()); +} + +/** + * Parses a Backstage-issued token and decorates the + * {@link @backstage/plugin-auth-node#BackstageIdentityResponse} with identity information sourced from the + * token. + * + * @public + */ +export function prepareBackstageIdentityResponse( + result: BackstageSignInResult, +): BackstageIdentityResponse { + if (!result.token) { + throw new InputError(`Identity response must return a token`); + } + + const { sub, ent = [], exp: expStr } = parseJwtPayload(result.token); + if (!sub) { + throw new InputError( + `Identity response must return a token with subject claim`, + ); + } + + const expAt = Number(expStr); + + // Default to 1 hour if no expiration is set, in particular to make testing simpler + const exp = expAt ? Math.round(expAt - Date.now() / 1000) : undefined; + if (exp && exp < 0) { + throw new InputError(`Identity response must not return an expired token`); + } + + return { + ...result, + expiresInSeconds: exp, + identity: { + type: 'user', + userEntityRef: sub, + ownershipEntityRefs: ent, + }, + }; +} diff --git a/plugins/auth-node/src/index.ts b/plugins/auth-node/src/index.ts index a70f667adf..e7b0b628b8 100644 --- a/plugins/auth-node/src/index.ts +++ b/plugins/auth-node/src/index.ts @@ -20,14 +20,27 @@ * @packageDocumentation */ -export { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; -export { DefaultIdentityClient } from './DefaultIdentityClient'; -export { IdentityClient } from './IdentityClient'; -export type { IdentityApi } from './IdentityApi'; -export type { IdentityClientOptions } from './DefaultIdentityClient'; +export * from './extensions'; +export * from './flow'; +export * from './identity'; +export * from './oauth'; +export * from './passport'; +export * from './proxy'; +export * from './sign-in'; export type { + AuthProviderConfig, + AuthProviderRouteHandlers, + AuthProviderFactory, + AuthResolverCatalogUserQuery, + AuthResolverContext, BackstageIdentityResponse, BackstageSignInResult, BackstageUserIdentity, - IdentityApiGetIdentityRequest, + ClientAuthResponse, + CookieConfigurer, + ProfileInfo, + ProfileTransform, + SignInInfo, + SignInResolver, + TokenParams, } from './types'; diff --git a/plugins/auth-node/src/oauth/OAuthCookieManager.ts b/plugins/auth-node/src/oauth/OAuthCookieManager.ts new file mode 100644 index 0000000000..5fe2304610 --- /dev/null +++ b/plugins/auth-node/src/oauth/OAuthCookieManager.ts @@ -0,0 +1,127 @@ +/* + * 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 { Request, Response } from 'express'; +import { CookieConfigurer } from '../types'; + +const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; +const TEN_MINUTES_MS = 600 * 1000; + +const defaultCookieConfigurer: CookieConfigurer = ({ + callbackUrl, + providerId, + appOrigin, +}) => { + const { hostname: domain, pathname, protocol } = new URL(callbackUrl); + const secure = protocol === 'https:'; + + // For situations where the auth-backend is running on a + // different domain than the app, we set the SameSite attribute + // to 'none' to allow third-party access to the cookie, but + // only if it's in a secure context (https). + let sameSite: ReturnType['sameSite'] = 'lax'; + if (new URL(appOrigin).hostname !== domain && secure) { + sameSite = 'none'; + } + + // If the provider supports callbackUrls, the pathname will + // contain the complete path to the frame handler so we need + // to slice off the trailing part of the path. + const path = pathname.endsWith(`${providerId}/handler/frame`) + ? pathname.slice(0, -'/handler/frame'.length) + : `${pathname}/${providerId}`; + + return { domain, path, secure, sameSite }; +}; + +/** @internal */ +export class OAuthCookieManager { + private readonly cookieConfigurer: CookieConfigurer; + private readonly nonceCookie: string; + private readonly refreshTokenCookie: string; + private readonly grantedScopeCookie: string; + + constructor( + private readonly options: { + providerId: string; + defaultAppOrigin: string; + baseUrl: string; + callbackUrl: string; + cookieConfigurer?: CookieConfigurer; + }, + ) { + this.cookieConfigurer = options.cookieConfigurer ?? defaultCookieConfigurer; + + this.nonceCookie = `${options.providerId}-nonce`; + this.refreshTokenCookie = `${options.providerId}-refresh-token`; + this.grantedScopeCookie = `${options.providerId}-granted-scope`; + } + + private getConfig(origin?: string, pathSuffix: string = '') { + const cookieConfig = this.cookieConfigurer({ + providerId: this.options.providerId, + baseUrl: this.options.baseUrl, + callbackUrl: this.options.callbackUrl, + appOrigin: origin ?? this.options.defaultAppOrigin, + }); + return { + httpOnly: true, + sameSite: 'lax' as const, + ...cookieConfig, + path: cookieConfig.path + pathSuffix, + }; + } + + setNonce(res: Response, nonce: string, origin?: string) { + res.cookie(this.nonceCookie, nonce, { + maxAge: TEN_MINUTES_MS, + ...this.getConfig(origin, '/handler'), + }); + } + + setRefreshToken(res: Response, refreshToken: string, origin?: string) { + res.cookie(this.refreshTokenCookie, refreshToken, { + maxAge: THOUSAND_DAYS_MS, + ...this.getConfig(origin), + }); + } + + removeRefreshToken(res: Response, origin?: string) { + res.cookie(this.refreshTokenCookie, '', { + maxAge: 0, + ...this.getConfig(origin), + }); + } + + setGrantedScopes(res: Response, scope: string, origin?: string) { + res.cookie(this.grantedScopeCookie, scope, { + maxAge: THOUSAND_DAYS_MS, + ...this.getConfig(origin), + }); + } + + getNonce(req: Request) { + return req.cookies[this.nonceCookie]; + } + + getRefreshToken(req: Request) { + return req.cookies[this.refreshTokenCookie]; + } + + getGrantedScopes(req: Request) { + return req.cookies[this.grantedScopeCookie]; + } +} diff --git a/plugins/auth-node/src/oauth/OAuthEnvironmentHandler.ts b/plugins/auth-node/src/oauth/OAuthEnvironmentHandler.ts new file mode 100644 index 0000000000..3a5ee00f8b --- /dev/null +++ b/plugins/auth-node/src/oauth/OAuthEnvironmentHandler.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { Config } from '@backstage/config'; +import { InputError, NotFoundError } from '@backstage/errors'; +import { AuthProviderRouteHandlers } from '../types'; +import { decodeOAuthState } from './state'; + +/** @public */ +export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { + static mapConfig( + config: Config, + factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, + ) { + const envs = config.keys(); + const handlers = new Map(); + + for (const env of envs) { + const envConfig = config.getConfig(env); + const handler = factoryFunc(envConfig); + handlers.set(env, handler); + } + + return new OAuthEnvironmentHandler(handlers); + } + + constructor( + private readonly handlers: Map, + ) {} + + async start(req: express.Request, res: express.Response): Promise { + const provider = this.getProviderForEnv(req); + await provider.start(req, res); + } + + async frameHandler( + req: express.Request, + res: express.Response, + ): Promise { + const provider = this.getProviderForEnv(req); + await provider.frameHandler(req, res); + } + + async refresh(req: express.Request, res: express.Response): Promise { + const provider = this.getProviderForEnv(req); + await provider.refresh?.(req, res); + } + + async logout(req: express.Request, res: express.Response): Promise { + const provider = this.getProviderForEnv(req); + await provider.logout?.(req, res); + } + + private getEnvFromRequest(req: express.Request): string | undefined { + const reqEnv = req.query.env?.toString(); + if (reqEnv) { + return reqEnv; + } + const stateParams = req.query.state?.toString(); + if (!stateParams) { + return undefined; + } + const { env } = decodeOAuthState(stateParams); + return env; + } + + private getProviderForEnv(req: express.Request): AuthProviderRouteHandlers { + const env: string | undefined = this.getEnvFromRequest(req); + + if (!env) { + throw new InputError(`Must specify 'env' query to select environment`); + } + + const handler = this.handlers.get(env); + if (!handler) { + throw new NotFoundError( + `No configuration available for the '${env}' environment of this provider.`, + ); + } + + return handler; + } +} diff --git a/plugins/auth-node/src/oauth/PassportOAuthAuthenticatorHelper.ts b/plugins/auth-node/src/oauth/PassportOAuthAuthenticatorHelper.ts new file mode 100644 index 0000000000..420a661da5 --- /dev/null +++ b/plugins/auth-node/src/oauth/PassportOAuthAuthenticatorHelper.ts @@ -0,0 +1,137 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Strategy } from 'passport'; +import { + PassportDoneCallback, + PassportHelpers, + PassportProfile, +} from '../passport'; +import { ProfileTransform } from '../types'; +import { + OAuthAuthenticatorAuthenticateInput, + OAuthAuthenticatorRefreshInput, + OAuthAuthenticatorResult, + OAuthAuthenticatorStartInput, +} from './types'; + +/** @public */ +export type PassportOAuthResult = { + fullProfile: PassportProfile; + params: { + id_token?: string; + scope: string; + token_type?: string; + expires_in: number; + }; + accessToken: string; +}; + +/** @public */ +export type PassportOAuthPrivateInfo = { + refreshToken?: string; +}; + +/** @public */ +export type PassportOAuthDoneCallback = PassportDoneCallback< + PassportOAuthResult, + PassportOAuthPrivateInfo +>; + +/** @public */ +export class PassportOAuthAuthenticatorHelper { + static defaultProfileTransform: ProfileTransform< + OAuthAuthenticatorResult + > = async input => ({ + profile: PassportHelpers.transformProfile( + input.fullProfile, + input.session.idToken, + ), + }); + + static from(strategy: Strategy) { + return new PassportOAuthAuthenticatorHelper(strategy); + } + + readonly #strategy: Strategy; + + private constructor(strategy: Strategy) { + this.#strategy = strategy; + } + + async start( + input: OAuthAuthenticatorStartInput, + options: Record, + ): Promise<{ url: string; status?: number }> { + return PassportHelpers.executeRedirectStrategy(input.req, this.#strategy, { + scope: input.scope, + state: input.state, + ...options, + }); + } + + async authenticate( + input: OAuthAuthenticatorAuthenticateInput, + ): Promise> { + const { result, privateInfo } = + await PassportHelpers.executeFrameHandlerStrategy< + PassportOAuthResult, + PassportOAuthPrivateInfo + >(input.req, this.#strategy); + + return { + fullProfile: result.fullProfile as PassportProfile, + session: { + accessToken: result.accessToken, + tokenType: result.params.token_type ?? 'bearer', + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + idToken: result.params.id_token, + refreshToken: privateInfo.refreshToken, + }, + }; + } + + async refresh( + input: OAuthAuthenticatorRefreshInput, + ): Promise> { + const result = await PassportHelpers.executeRefreshTokenStrategy( + this.#strategy, + input.refreshToken, + input.scope, + ); + const fullProfile = await this.fetchProfile(result.accessToken); + return { + fullProfile, + session: { + accessToken: result.accessToken, + tokenType: result.params.token_type ?? 'bearer', + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + idToken: result.params.id_token, + refreshToken: result.refreshToken, + }, + }; + } + + async fetchProfile(accessToken: string): Promise { + const profile = await PassportHelpers.executeFetchUserProfileStrategy( + this.#strategy, + accessToken, + ); + return profile; + } +} diff --git a/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts b/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts new file mode 100644 index 0000000000..87fde6a1a2 --- /dev/null +++ b/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts @@ -0,0 +1,66 @@ +/* + * 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 { readDeclarativeSignInResolver } from '../sign-in'; +import { + AuthProviderFactory, + ProfileTransform, + SignInResolver, +} from '../types'; +import { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler'; +import { createOAuthRouteHandlers } from './createOAuthRouteHandlers'; +import { OAuthStateTransform } from './state'; +import { OAuthAuthenticator, OAuthAuthenticatorResult } from './types'; +import { SignInResolverFactory } from '../sign-in/createSignInResolverFactory'; + +/** @public */ +export function createOAuthProviderFactory(options: { + authenticator: OAuthAuthenticator; + stateTransform?: OAuthStateTransform; + profileTransform?: ProfileTransform>; + signInResolver?: SignInResolver>; + signInResolverFactories?: { + [name in string]: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; + }; +}): AuthProviderFactory { + return ctx => { + return OAuthEnvironmentHandler.mapConfig(ctx.config, envConfig => { + const signInResolver = + options.signInResolver ?? + readDeclarativeSignInResolver({ + config: envConfig, + signInResolverFactories: options.signInResolverFactories ?? {}, + }); + + return createOAuthRouteHandlers({ + authenticator: options.authenticator, + appUrl: ctx.appUrl, + baseUrl: ctx.baseUrl, + config: envConfig, + isOriginAllowed: ctx.isOriginAllowed, + cookieConfigurer: ctx.cookieConfigurer, + providerId: ctx.providerId, + resolverContext: ctx.resolverContext, + stateTransform: options.stateTransform, + profileTransform: options.profileTransform, + signInResolver, + }); + }); + }; +} diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts new file mode 100644 index 0000000000..2573d95ea1 --- /dev/null +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts @@ -0,0 +1,742 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import express from 'express'; +import request, { SuperAgentTest } from 'supertest'; +import cookieParser from 'cookie-parser'; +import PromiseRouter from 'express-promise-router'; +import { AuthProviderRouteHandlers, AuthResolverContext } from '../types'; +import { createOAuthRouteHandlers } from './createOAuthRouteHandlers'; +import { OAuthAuthenticator } from './types'; +import { errorHandler } from '@backstage/backend-common'; +import { encodeOAuthState, OAuthState } from './state'; +import { PassportProfile } from '../passport'; +import { parseWebMessageResponse } from '../flow/__testUtils__/parseWebMessageResponse'; + +const mockAuthenticator: jest.Mocked> = { + initialize: jest.fn(_r => ({ ctx: 'authenticator' })), + start: jest.fn(), + authenticate: jest.fn(), + refresh: jest.fn(), + logout: jest.fn(), + defaultProfileTransform: jest.fn(async (_r, _c) => ({ profile: {} })), +}; + +const mockBackstageToken = `a.${btoa( + JSON.stringify({ sub: 'user:default/mock', ent: [] }), +)}.c`; + +const mockSession = { + accessToken: 'access-token', + expiresInSeconds: 3, + scope: 'my-scope', + tokenType: 'bear', + idToken: 'id-token', + refreshToken: 'refresh-token', +}; + +const baseConfig = { + authenticator: mockAuthenticator, + appUrl: 'http://127.0.0.1', + baseUrl: 'http://127.0.0.1:7007', + isOriginAllowed: () => true, + providerId: 'my-provider', + config: new ConfigReader({}), + resolverContext: { ctx: 'resolver' } as unknown as AuthResolverContext, +}; + +function wrapInApp(handlers: AuthProviderRouteHandlers) { + const app = express(); + + const router = PromiseRouter(); + + router.use(cookieParser()); + app.use('/my-provider', router); + app.use(errorHandler()); + + router.get('/start', handlers.start.bind(handlers)); + router.get('/handler/frame', handlers.frameHandler.bind(handlers)); + router.post('/handler/frame', handlers.frameHandler.bind(handlers)); + if (handlers.logout) { + router.post('/logout', handlers.logout.bind(handlers)); + } + if (handlers.refresh) { + router.get('/refresh', handlers.refresh.bind(handlers)); + router.post('/refresh', handlers.refresh.bind(handlers)); + } + + return app; +} + +function getNonceCookie(test: SuperAgentTest) { + return test.jar.getCookie('my-provider-nonce', { + domain: '127.0.0.1', + path: '/my-provider/handler', + script: false, + secure: false, + }); +} + +function getRefreshTokenCookie(test: SuperAgentTest) { + return test.jar.getCookie('my-provider-refresh-token', { + domain: '127.0.0.1', + path: '/my-provider', + script: false, + secure: false, + }); +} + +function getGrantedScopesCookie(test: SuperAgentTest) { + return test.jar.getCookie('my-provider-granted-scope', { + domain: '127.0.0.1', + path: '/my-provider', + script: false, + secure: false, + }); +} + +describe('createOAuthRouteHandlers', () => { + afterEach(() => jest.clearAllMocks()); + + it('should be created', () => { + const handlers = createOAuthRouteHandlers(baseConfig); + expect(handlers).toEqual({ + start: expect.any(Function), + frameHandler: expect.any(Function), + refresh: expect.any(Function), + logout: expect.any(Function), + }); + }); + + describe('start', () => { + it('should require an env query', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app).get('/my-provider/start'); + + expect(res.status).toBe(400); + expect(res.body).toMatchObject({ + error: { + name: 'InputError', + message: 'No env provided in request query parameters', + }, + }); + }); + + it('should start', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + mockAuthenticator.start.mockResolvedValue({ + url: 'https://example.com/redirect', + }); + + const res = await agent.get( + '/my-provider/start?env=development&scope=my-scope', + ); + + const { value: nonce } = getNonceCookie(agent); + + expect(res.text).toBe(''); + expect(res.status).toBe(302); + expect(res.get('Location')).toBe('https://example.com/redirect'); + expect(res.get('Content-Length')).toBe('0'); + + expect(mockAuthenticator.start).toHaveBeenCalledWith( + { + req: expect.anything(), + scope: 'my-scope', + state: encodeOAuthState({ + nonce: decodeURIComponent(nonce), + env: 'development', + }), + }, + { ctx: 'authenticator' }, + ); + }); + + it('should start with additional parameters, transform state, and persist scopes', async () => { + const agent = request.agent( + wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + authenticator: { + ...mockAuthenticator, + shouldPersistScopes: true, + }, + stateTransform: async state => ({ + state: { ...state, nonce: '123' }, + }), + }), + ), + ); + + mockAuthenticator.start.mockResolvedValue({ + url: 'https://example.com/redirect', + }); + + const res = await agent.get('/my-provider/start').query({ + env: 'development', + scope: 'my-scope', + origin: 'https://remotehost', + redirectUrl: 'https://remotehost/redirect', + flow: 'redirect', + }); + + expect(res.text).toBe(''); + expect(res.status).toBe(302); + expect(res.get('Location')).toBe('https://example.com/redirect'); + expect(res.get('Content-Length')).toBe('0'); + + expect(mockAuthenticator.start).toHaveBeenCalledWith( + { + req: expect.anything(), + scope: 'my-scope', + state: encodeOAuthState({ + nonce: '123', + env: 'development', + origin: 'https://remotehost', + redirectUrl: 'https://remotehost/redirect', + flow: 'redirect', + scope: 'my-scope', + }), + }, + { ctx: 'authenticator' }, + ); + }); + }); + + describe('frameHandler', () => { + it('should authenticate', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-nonce=123', + '127.0.0.1', + '/my-provider/handler', + ); + + mockAuthenticator.authenticate.mockResolvedValue({ + fullProfile: { id: 'id' } as PassportProfile, + session: mockSession, + }); + + const res = await agent.get('/my-provider/handler/frame').query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + } as OAuthState), + }); + + expect(mockAuthenticator.authenticate).toHaveBeenCalledWith( + { req: expect.anything() }, + { ctx: 'authenticator' }, + ); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + response: { + profile: {}, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'my-scope', + }, + }, + }); + + expect(getRefreshTokenCookie(agent).value).toBe('refresh-token'); + expect(getGrantedScopesCookie(agent)).toBeUndefined(); + }); + + it('should authenticate with sign-in, profile transform, and persisted scopes', async () => { + const agent = request.agent( + wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + authenticator: { + ...mockAuthenticator, + shouldPersistScopes: true, + }, + profileTransform: async () => ({ profile: { email: 'em@i.l' } }), + signInResolver: async () => ({ token: mockBackstageToken }), + }), + ), + ); + + agent.jar.setCookie( + 'my-provider-nonce=123', + '127.0.0.1', + '/my-provider/handler', + ); + + mockAuthenticator.authenticate.mockResolvedValue({ + fullProfile: { id: 'id' } as PassportProfile, + session: mockSession, + }); + + const res = await agent.get('/my-provider/handler/frame').query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + scope: 'my-scope', + } as OAuthState), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + response: { + profile: { email: 'em@i.l' }, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'my-scope', + }, + backstageIdentity: { + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user:default/mock', + }, + token: mockBackstageToken, + }, + }, + }); + + expect(getRefreshTokenCookie(agent).value).toBe('refresh-token'); + expect(getGrantedScopesCookie(agent).value).toBe('my-scope'); + }); + + it('should redirect with persisted scope', async () => { + const agent = request.agent( + wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + authenticator: { + ...mockAuthenticator, + shouldPersistScopes: true, + }, + profileTransform: async () => ({ profile: { email: 'em@i.l' } }), + signInResolver: async () => ({ token: mockBackstageToken }), + }), + ), + ); + + agent.jar.setCookie( + 'my-provider-nonce=123', + '127.0.0.1', + '/my-provider/handler', + ); + + mockAuthenticator.authenticate.mockResolvedValue({ + fullProfile: { id: 'id' } as PassportProfile, + session: mockSession, + }); + + const res = await agent.get('/my-provider/handler/frame').query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + scope: 'my-scope', + flow: 'redirect', + redirectUrl: 'https://127.0.0.1:3000/redirect', + } as OAuthState), + }); + + expect(res.status).toBe(302); + expect(res.get('Location')).toBe('https://127.0.0.1:3000/redirect'); + + expect(getRefreshTokenCookie(agent).value).toBe('refresh-token'); + expect(getGrantedScopesCookie(agent).value).toBe('my-scope'); + }); + + it('should require a valid origin', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app) + .get('/my-provider/handler/frame') + .query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + origin: 'invalid-origin', + }), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: 'App origin is invalid, failed to parse', + }, + }); + }); + + it('should reject origins that are not allowed', async () => { + const app = wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + isOriginAllowed: () => false, + }), + ); + const res = await request(app) + .get('/my-provider/handler/frame') + .query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + origin: 'http://localhost:3000', + }), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: "Origin 'http://localhost:3000' is not allowed", + }, + }); + }); + + it('should reject missing state env', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app) + .get('/my-provider/handler/frame') + .query({ + state: encodeOAuthState({ + nonce: '123', + } as OAuthState), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: 'OAuth state is invalid, missing env', + }, + }); + }); + + it('should reject missing cookie nonce', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app) + .get('/my-provider/handler/frame') + .query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + }), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: 'Auth response is missing cookie nonce', + }, + }); + }); + + it('should reject missing state nonce', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app) + .get('/my-provider/handler/frame') + .query({ + state: encodeOAuthState({ + env: 'development', + } as OAuthState), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: 'OAuth state is invalid, missing nonce', + }, + }); + }); + + it('should reject mismatched nonce', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-nonce=456', + '127.0.0.1', + '/my-provider/handler', + ); + + const res = await agent.get('/my-provider/handler/frame').query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + } as OAuthState), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: 'Invalid nonce', + }, + }); + }); + }); + + describe('refresh', () => { + it('should refresh', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { ...mockSession, scope }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest') + .query({ scope: 'my-scope' }); + + expect(mockAuthenticator.refresh).toHaveBeenCalledWith( + { + req: expect.anything(), + refreshToken: 'refresh-token', + scope: 'my-scope', + }, + { ctx: 'authenticator' }, + ); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + profile: {}, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'my-scope', + }, + }); + }); + + it('should refresh with sign-in, profile transform, and persisted scopes', async () => { + const agent = request.agent( + wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + authenticator: { + ...mockAuthenticator, + shouldPersistScopes: true, + }, + profileTransform: async () => ({ profile: { email: 'em@i.l' } }), + signInResolver: async () => ({ token: mockBackstageToken }), + }), + ), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + agent.jar.setCookie( + 'my-provider-granted-scope=persisted-scope', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { ...mockSession, scope, refreshToken: 'new-refresh-token' }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(mockAuthenticator.refresh).toHaveBeenCalledWith( + { + req: expect.anything(), + refreshToken: 'refresh-token', + scope: 'persisted-scope', + }, + { ctx: 'authenticator' }, + ); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + profile: { email: 'em@i.l' }, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'persisted-scope', + }, + backstageIdentity: { + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user:default/mock', + }, + token: mockBackstageToken, + }, + }); + expect(getRefreshTokenCookie(agent).value).toBe('new-refresh-token'); + }); + + it('should forward errors', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockRejectedValue(new Error('NOPE')); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Refresh failed; caused by Error: NOPE', + }, + }); + }); + + it('should require refresh cookie', async () => { + const res = await request(wrapInApp(createOAuthRouteHandlers(baseConfig))) + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: + 'Refresh failed; caused by InputError: Missing session cookie', + }, + }); + }); + + it('should reject requests without CSRF header', async () => { + const res = await request( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ).post('/my-provider/refresh'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + + it('should reject requests with invalid CSRF header', async () => { + const res = await request(wrapInApp(createOAuthRouteHandlers(baseConfig))) + .post('/my-provider/refresh') + .set('X-Requested-With', 'invalid'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + }); + + describe('logout', () => { + it('should log out', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=my-refresh-token', + '127.0.0.1', + '/my-provider', + ); + + expect(getRefreshTokenCookie(agent).value).toBe('my-refresh-token'); + + const res = await agent + .post('/my-provider/logout') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(200); + expect(res.body).toEqual({}); + + expect(getRefreshTokenCookie(agent)).toBeUndefined(); + }); + + it('should reject requests without CSRF header', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + + const res = await request(app).post('/my-provider/logout'); + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + + it('should reject requests with invalid CSRF header', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + + const res = await request(app) + .post('/my-provider/logout') + .set('X-Requested-With', 'wrong-value'); + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + }); +}); diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts new file mode 100644 index 0000000000..011b3fc0f0 --- /dev/null +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts @@ -0,0 +1,343 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import crypto from 'crypto'; +import { URL } from 'url'; +import { + AuthenticationError, + InputError, + isError, + NotAllowedError, +} from '@backstage/errors'; +import { + encodeOAuthState, + decodeOAuthState, + OAuthStateTransform, + OAuthState, +} from './state'; +import { sendWebMessageResponse } from '../flow'; +import { prepareBackstageIdentityResponse } from '../identity'; +import { OAuthCookieManager } from './OAuthCookieManager'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + CookieConfigurer, + ProfileTransform, + SignInResolver, +} from '../types'; +import { OAuthAuthenticator, OAuthAuthenticatorResult } from './types'; +import { Config } from '@backstage/config'; + +/** @public */ +export interface OAuthRouteHandlersOptions { + authenticator: OAuthAuthenticator; + appUrl: string; + baseUrl: string; + isOriginAllowed: (origin: string) => boolean; + providerId: string; + config: Config; + resolverContext: AuthResolverContext; + stateTransform?: OAuthStateTransform; + profileTransform?: ProfileTransform>; + cookieConfigurer?: CookieConfigurer; + signInResolver?: SignInResolver>; +} + +/** @internal */ +type ClientOAuthResponse = ClientAuthResponse<{ + /** + * An access token issued for the signed in user. + */ + accessToken: string; + /** + * (Optional) Id token issued for the signed in user. + */ + idToken?: string; + /** + * Expiry of the access token in seconds. + */ + expiresInSeconds?: number; + /** + * Scopes granted for the access token. + */ + scope: string; +}>; + +/** @public */ +export function createOAuthRouteHandlers( + options: OAuthRouteHandlersOptions, +): AuthProviderRouteHandlers { + const { + authenticator, + config, + baseUrl, + appUrl, + providerId, + isOriginAllowed, + cookieConfigurer, + resolverContext, + signInResolver, + } = options; + + const defaultAppOrigin = new URL(appUrl).origin; + const callbackUrl = + config.getOptionalString('callbackUrl') ?? + `${baseUrl}/${providerId}/handler/frame`; + + const stateTransform = options.stateTransform ?? (state => ({ state })); + const profileTransform = + options.profileTransform ?? authenticator.defaultProfileTransform; + const authenticatorCtx = authenticator.initialize({ config, callbackUrl }); + const cookieManager = new OAuthCookieManager({ + baseUrl, + callbackUrl, + defaultAppOrigin, + providerId, + cookieConfigurer, + }); + + return { + async start( + this: never, + req: express.Request, + res: express.Response, + ): Promise { + // retrieve scopes from request + const scope = req.query.scope?.toString() ?? ''; + const env = req.query.env?.toString(); + const origin = req.query.origin?.toString(); + const redirectUrl = req.query.redirectUrl?.toString(); + const flow = req.query.flow?.toString(); + + if (!env) { + throw new InputError('No env provided in request query parameters'); + } + + const nonce = crypto.randomBytes(16).toString('base64'); + // set a nonce cookie before redirecting to oauth provider + cookieManager.setNonce(res, nonce, origin); + + const state: OAuthState = { nonce, env, origin, redirectUrl, flow }; + + // If scopes are persisted then we pass them through the state so that we + // can set the cookie on successful auth + if (authenticator.shouldPersistScopes) { + state.scope = scope; + } + + const { state: transformedState } = await stateTransform(state, { req }); + const encodedState = encodeOAuthState(transformedState); + + const { url, status } = await options.authenticator.start( + { req, scope, state: encodedState }, + authenticatorCtx, + ); + + res.statusCode = status || 302; + res.setHeader('Location', url); + res.setHeader('Content-Length', '0'); + res.end(); + }, + + async frameHandler( + this: never, + req: express.Request, + res: express.Response, + ): Promise { + let appOrigin = defaultAppOrigin; + + try { + const state = decodeOAuthState(req.query.state?.toString() ?? ''); + + if (state.origin) { + try { + appOrigin = new URL(state.origin).origin; + } catch { + throw new NotAllowedError('App origin is invalid, failed to parse'); + } + if (!isOriginAllowed(appOrigin)) { + throw new NotAllowedError(`Origin '${appOrigin}' is not allowed`); + } + } + + // The same nonce is passed through cookie and state, and they must match + const cookieNonce = cookieManager.getNonce(req); + const stateNonce = state.nonce; + if (!cookieNonce) { + throw new NotAllowedError('Auth response is missing cookie nonce'); + } + if (cookieNonce !== stateNonce) { + throw new NotAllowedError('Invalid nonce'); + } + + const result = await authenticator.authenticate( + { req }, + authenticatorCtx, + ); + const { profile } = await profileTransform(result, resolverContext); + + const response: ClientOAuthResponse = { + profile, + providerInfo: { + idToken: result.session.idToken, + accessToken: result.session.accessToken, + scope: result.session.scope, + expiresInSeconds: result.session.expiresInSeconds, + }, + }; + + if (signInResolver) { + const identity = await signInResolver( + { profile, result }, + resolverContext, + ); + response.backstageIdentity = + prepareBackstageIdentityResponse(identity); + } + + // Store the scope that we have been granted for this session. This is useful if + // the provider does not return granted scopes on refresh or if they are normalized. + if (authenticator.shouldPersistScopes && state.scope) { + cookieManager.setGrantedScopes(res, state.scope, appOrigin); + result.session.scope = state.scope; + } + + if (result.session.refreshToken) { + // set new refresh token + cookieManager.setRefreshToken( + res, + result.session.refreshToken, + appOrigin, + ); + } + + // When using the redirect flow we rely on refresh token we just + // acquired to get a new session once we're back in the app. + if (state.flow === 'redirect') { + if (!state.redirectUrl) { + throw new InputError( + 'No redirectUrl provided in request query parameters', + ); + } + res.redirect(state.redirectUrl); + return; + } + + // post message back to popup if successful + sendWebMessageResponse(res, appOrigin, { + type: 'authorization_response', + response, + }); + } catch (error) { + const { name, message } = isError(error) + ? error + : new Error('Encountered invalid error'); // Being a bit safe and not forwarding the bad value + // post error message back to popup if failure + sendWebMessageResponse(res, appOrigin, { + type: 'authorization_response', + error: { name, message }, + }); + } + }, + + async logout( + this: never, + req: express.Request, + res: express.Response, + ): Promise { + // We use this as a lightweight CSRF protection + if (req.header('X-Requested-With') !== 'XMLHttpRequest') { + throw new AuthenticationError('Invalid X-Requested-With header'); + } + + if (authenticator.logout) { + const refreshToken = cookieManager.getRefreshToken(req); + await authenticator.logout({ req, refreshToken }, authenticatorCtx); + } + + // remove refresh token cookie if it is set + cookieManager.removeRefreshToken(res, req.get('origin')); + + res.status(200).end(); + }, + + async refresh( + this: never, + req: express.Request, + res: express.Response, + ): Promise { + // We use this as a lightweight CSRF protection + if (req.header('X-Requested-With') !== 'XMLHttpRequest') { + throw new AuthenticationError('Invalid X-Requested-With header'); + } + + try { + const refreshToken = cookieManager.getRefreshToken(req); + + // throw error if refresh token is missing in the request + if (!refreshToken) { + throw new InputError('Missing session cookie'); + } + + let scope = req.query.scope?.toString() ?? ''; + if (authenticator.shouldPersistScopes) { + scope = cookieManager.getGrantedScopes(req); + } + + const result = await authenticator.refresh( + { req, scope, refreshToken }, + authenticatorCtx, + ); + + const { profile } = await profileTransform(result, resolverContext); + + const newRefreshToken = result.session.refreshToken; + if (newRefreshToken && newRefreshToken !== refreshToken) { + cookieManager.setRefreshToken( + res, + newRefreshToken, + req.get('origin'), + ); + } + + const response: ClientOAuthResponse = { + profile, + providerInfo: { + idToken: result.session.idToken, + accessToken: result.session.accessToken, + scope: result.session.scope, + expiresInSeconds: result.session.expiresInSeconds, + }, + }; + + if (signInResolver) { + const identity = await signInResolver( + { profile, result }, + resolverContext, + ); + response.backstageIdentity = + prepareBackstageIdentityResponse(identity); + } + + res.status(200).json(response); + } catch (error) { + throw new AuthenticationError('Refresh failed', error); + } + }, + }; +} diff --git a/plugins/auth-node/src/oauth/index.ts b/plugins/auth-node/src/oauth/index.ts new file mode 100644 index 0000000000..25213dd2f0 --- /dev/null +++ b/plugins/auth-node/src/oauth/index.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + createOAuthRouteHandlers, + type OAuthRouteHandlersOptions, +} from './createOAuthRouteHandlers'; +export { + PassportOAuthAuthenticatorHelper, + type PassportOAuthDoneCallback, + type PassportOAuthPrivateInfo, + type PassportOAuthResult, +} from './PassportOAuthAuthenticatorHelper'; +export { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler'; +export { createOAuthProviderFactory } from './createOAuthProviderFactory'; +export { + encodeOAuthState, + decodeOAuthState, + type OAuthState, + type OAuthStateTransform, +} from './state'; +export { + createOAuthAuthenticator, + type OAuthAuthenticator, + type OAuthAuthenticatorAuthenticateInput, + type OAuthAuthenticatorLogoutInput, + type OAuthAuthenticatorRefreshInput, + type OAuthAuthenticatorResult, + type OAuthAuthenticatorStartInput, + type OAuthSession, +} from './types'; diff --git a/plugins/auth-node/src/oauth/state.ts b/plugins/auth-node/src/oauth/state.ts new file mode 100644 index 0000000000..fc747d08a5 --- /dev/null +++ b/plugins/auth-node/src/oauth/state.ts @@ -0,0 +1,62 @@ +/* + * 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 pickBy from 'lodash/pickBy'; +import { Request } from 'express'; +import { NotAllowedError } from '@backstage/errors'; + +/** + * A type for the serialized value in the `state` parameter of the OAuth authorization flow + * @public + */ +export type OAuthState = { + nonce: string; + env: string; + origin?: string; + scope?: string; + redirectUrl?: string; + flow?: string; +}; + +/** @public */ +export type OAuthStateTransform = ( + state: OAuthState, + context: { req: Request }, +) => Promise<{ state: OAuthState }>; + +/** @public */ +export function encodeOAuthState(state: OAuthState): string { + const stateString = new URLSearchParams( + pickBy(state, value => value !== undefined), + ).toString(); + + return Buffer.from(stateString, 'utf-8').toString('hex'); +} + +/** @public */ +export function decodeOAuthState(encodedState: string): OAuthState { + const state = Object.fromEntries( + new URLSearchParams(Buffer.from(encodedState, 'hex').toString('utf-8')), + ); + if (!state.env || state.env?.length === 0) { + throw new NotAllowedError('OAuth state is invalid, missing env'); + } + if (!state.nonce || state.nonce?.length === 0) { + throw new NotAllowedError('OAuth state is invalid, missing nonce'); + } + + return state as OAuthState; +} diff --git a/plugins/auth-node/src/oauth/types.ts b/plugins/auth-node/src/oauth/types.ts new file mode 100644 index 0000000000..4bd7bd3f3f --- /dev/null +++ b/plugins/auth-node/src/oauth/types.ts @@ -0,0 +1,89 @@ +/* + * 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 { Config } from '@backstage/config'; +import { Request } from 'express'; +import { ProfileTransform } from '../types'; + +/** @public */ +export interface OAuthSession { + accessToken: string; + tokenType: string; + idToken?: string; + scope: string; + expiresInSeconds?: number; + refreshToken?: string; + refreshTokenExpiresInSeconds?: number; +} + +/** @public */ +export interface OAuthAuthenticatorStartInput { + scope: string; + state: string; + req: Request; +} + +/** @public */ +export interface OAuthAuthenticatorAuthenticateInput { + req: Request; +} + +/** @public */ +export interface OAuthAuthenticatorRefreshInput { + scope: string; + refreshToken: string; + req: Request; +} + +/** @public */ +export interface OAuthAuthenticatorLogoutInput { + accessToken?: string; + refreshToken?: string; + req: Request; +} + +/** @public */ +export interface OAuthAuthenticatorResult { + fullProfile: TProfile; + session: OAuthSession; +} + +/** @public */ +export interface OAuthAuthenticator { + defaultProfileTransform: ProfileTransform>; + shouldPersistScopes?: boolean; + initialize(ctx: { callbackUrl: string; config: Config }): TContext; + start( + input: OAuthAuthenticatorStartInput, + ctx: TContext, + ): Promise<{ url: string; status?: number }>; + authenticate( + input: OAuthAuthenticatorAuthenticateInput, + ctx: TContext, + ): Promise>; + refresh( + input: OAuthAuthenticatorRefreshInput, + ctx: TContext, + ): Promise>; + logout?(input: OAuthAuthenticatorLogoutInput, ctx: TContext): Promise; +} + +/** @public */ +export function createOAuthAuthenticator( + authenticator: OAuthAuthenticator, +): OAuthAuthenticator { + return authenticator; +} diff --git a/plugins/auth-node/src/passport/PassportHelpers.ts b/plugins/auth-node/src/passport/PassportHelpers.ts new file mode 100644 index 0000000000..6c13523811 --- /dev/null +++ b/plugins/auth-node/src/passport/PassportHelpers.ts @@ -0,0 +1,249 @@ +/* + * 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 { Request } from 'express'; +import { Strategy } from 'passport'; +import { PassportProfile } from './types'; +import { ProfileInfo } from '../types'; + +// Re-declared here to avoid direct dependency on passport-oauth2 +/** @internal */ +interface InternalOAuthError extends Error { + oauthError?: { + data?: string; + }; +} + +/** @internal */ +function decodeJwtPayload(token: string): Record { + const payloadStr = token.split('.')[1]; + if (!payloadStr) { + throw new Error('Invalid JWT token'); + } + + let payload: unknown; + try { + payload = JSON.parse( + Buffer.from( + payloadStr.replace(/-/g, '+').replace(/_/g, '/'), + 'base64', + ).toString('utf8'), + ); + } catch (e) { + throw new Error('Invalid JWT token'); + } + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + throw new Error('Invalid JWT token'); + } + return payload as Record; +} + +/** @public */ +export class PassportHelpers { + private constructor() {} + + static transformProfile = ( + profile: PassportProfile, + idToken?: string, + ): ProfileInfo => { + let email: string | undefined = undefined; + if (profile.emails && profile.emails.length > 0) { + const [firstEmail] = profile.emails; + email = firstEmail.value; + } + + let picture: string | undefined = undefined; + if (profile.avatarUrl) { + picture = profile.avatarUrl; + } else if (profile.photos && profile.photos.length > 0) { + const [firstPhoto] = profile.photos; + picture = firstPhoto.value; + } + + let displayName: string | undefined = + profile.displayName ?? profile.username ?? profile.id; + + if ((!email || !picture || !displayName) && idToken) { + try { + const decoded: Record = decodeJwtPayload(idToken); + if (!email && decoded.email) { + email = decoded.email; + } + if (!picture && decoded.picture) { + picture = decoded.picture; + } + if (!displayName && decoded.name) { + displayName = decoded.name; + } + } catch (e) { + throw new Error(`Failed to parse id token and get profile info, ${e}`); + } + } + + return { + email, + picture, + displayName, + }; + }; + + static async executeRedirectStrategy( + req: Request, + providerStrategy: Strategy, + options: Record, + ): Promise<{ + /** + * URL to redirect to + */ + url: string; + /** + * Status code to use for the redirect + */ + status?: number; + }> { + return new Promise(resolve => { + const strategy = Object.create(providerStrategy); + strategy.redirect = (url: string, status?: number) => { + resolve({ url, status: status ?? undefined }); + }; + + strategy.authenticate(req, { ...options }); + }); + } + + static async executeFrameHandlerStrategy( + req: Request, + providerStrategy: Strategy, + options?: Record, + ): Promise<{ result: TResult; privateInfo: TPrivateInfo }> { + return new Promise((resolve, reject) => { + const strategy = Object.create(providerStrategy); + strategy.success = (result: any, privateInfo: any) => { + resolve({ result, privateInfo }); + }; + strategy.fail = ( + info: { type: 'success' | 'error'; message?: string }, + // _status: number, + ) => { + reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); + }; + strategy.error = (error: InternalOAuthError) => { + let message = `Authentication failed, ${error.message}`; + + if (error.oauthError?.data) { + try { + const errorData = JSON.parse(error.oauthError.data); + + if (errorData.message) { + message += ` - ${errorData.message}`; + } + } catch (parseError) { + message += ` - ${error.oauthError}`; + } + } + + reject(new Error(message)); + }; + strategy.redirect = () => { + reject(new Error('Unexpected redirect')); + }; + strategy.authenticate(req, { ...(options ?? {}) }); + }); + } + + static async executeRefreshTokenStrategy( + providerStrategy: Strategy, + refreshToken: string, + scope: string, + ): Promise<{ + /** + * An access token issued for the signed in user. + */ + accessToken: string; + /** + * Optionally, the server can issue a new Refresh Token for the user + */ + refreshToken?: string; + params: any; + }> { + return new Promise((resolve, reject) => { + const anyStrategy = providerStrategy as any; + const OAuth2 = anyStrategy._oauth2.constructor; + const oauth2 = new OAuth2( + anyStrategy._oauth2._clientId, + anyStrategy._oauth2._clientSecret, + anyStrategy._oauth2._baseSite, + anyStrategy._oauth2._authorizeUrl, + anyStrategy._refreshURL || anyStrategy._oauth2._accessTokenUrl, + anyStrategy._oauth2._customHeaders, + ); + + oauth2.getOAuthAccessToken( + refreshToken, + { + scope, + grant_type: 'refresh_token', + }, + ( + err: Error | null, + accessToken: string, + newRefreshToken: string, + params: any, + ) => { + if (err) { + reject( + new Error(`Failed to refresh access token ${err.toString()}`), + ); + } + if (!accessToken) { + reject( + new Error( + `Failed to refresh access token, no access token received`, + ), + ); + } + + resolve({ + accessToken, + refreshToken: newRefreshToken, + params, + }); + }, + ); + }); + } + + static async executeFetchUserProfileStrategy( + providerStrategy: Strategy, + accessToken: string, + ): Promise { + return new Promise((resolve, reject) => { + const anyStrategy = providerStrategy as unknown as { + userProfile(accessToken: string, callback: Function): void; + }; + anyStrategy.userProfile( + accessToken, + (error: Error, rawProfile: PassportProfile) => { + if (error) { + reject(error); + } else { + resolve(rawProfile); + } + }, + ); + }); + } +} diff --git a/plugins/auth-node/src/passport/index.ts b/plugins/auth-node/src/passport/index.ts new file mode 100644 index 0000000000..f70c0af921 --- /dev/null +++ b/plugins/auth-node/src/passport/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { PassportHelpers } from './PassportHelpers'; +export type { PassportDoneCallback, PassportProfile } from './types'; diff --git a/plugins/auth-node/src/passport/types.ts b/plugins/auth-node/src/passport/types.ts new file mode 100644 index 0000000000..4a3f41b6b2 --- /dev/null +++ b/plugins/auth-node/src/passport/types.ts @@ -0,0 +1,29 @@ +/* + * 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 { Profile } from 'passport'; + +/** @public */ +export type PassportProfile = Profile & { + avatarUrl?: string; +}; + +/** @public */ +export type PassportDoneCallback = ( + err?: Error, + result?: TResult, + privateInfo?: TPrivateInfo, +) => void; diff --git a/plugins/auth-node/src/proxy/createProxyAuthProviderFactory.ts b/plugins/auth-node/src/proxy/createProxyAuthProviderFactory.ts new file mode 100644 index 0000000000..8626c40b33 --- /dev/null +++ b/plugins/auth-node/src/proxy/createProxyAuthProviderFactory.ts @@ -0,0 +1,61 @@ +/* + * 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 { + readDeclarativeSignInResolver, + SignInResolverFactory, +} from '../sign-in'; +import { + AuthProviderFactory, + ProfileTransform, + SignInResolver, +} from '../types'; +import { createProxyAuthRouteHandlers } from './createProxyRouteHandlers'; +import { ProxyAuthenticator } from './types'; + +/** @public */ +export function createProxyAuthProviderFactory(options: { + authenticator: ProxyAuthenticator; + profileTransform?: ProfileTransform; + signInResolver?: SignInResolver; + signInResolverFactories?: Record< + string, + SignInResolverFactory + >; +}): AuthProviderFactory { + return ctx => { + const signInResolver = + options.signInResolver ?? + readDeclarativeSignInResolver({ + config: ctx.config, + signInResolverFactories: options.signInResolverFactories ?? {}, + }); + + if (!signInResolver) { + throw new Error( + `No sign-in resolver configured for proxy auth provider '${ctx.providerId}'`, + ); + } + + return createProxyAuthRouteHandlers({ + signInResolver, + config: ctx.config, + authenticator: options.authenticator, + resolverContext: ctx.resolverContext, + profileTransform: options.profileTransform, + }); + }; +} diff --git a/plugins/auth-node/src/proxy/createProxyRouteHandlers.ts b/plugins/auth-node/src/proxy/createProxyRouteHandlers.ts new file mode 100644 index 0000000000..521fe39288 --- /dev/null +++ b/plugins/auth-node/src/proxy/createProxyRouteHandlers.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Request, Response } from 'express'; +import { Config } from '@backstage/config'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + ProfileTransform, + SignInResolver, +} from '../types'; +import { ProxyAuthenticator } from './types'; +import { prepareBackstageIdentityResponse } from '../identity'; +import { NotImplementedError } from '@backstage/errors'; + +/** @public */ +export interface ProxyAuthRouteHandlersOptions { + authenticator: ProxyAuthenticator; + config: Config; + resolverContext: AuthResolverContext; + signInResolver: SignInResolver; + profileTransform?: ProfileTransform; +} + +/** @public */ +export function createProxyAuthRouteHandlers( + options: ProxyAuthRouteHandlersOptions, +): AuthProviderRouteHandlers { + const { authenticator, config, resolverContext, signInResolver } = options; + + const profileTransform = + options.profileTransform ?? authenticator.defaultProfileTransform; + const authenticatorCtx = authenticator.initialize({ config }); + + return { + async start(): Promise { + throw new NotImplementedError('Not implemented'); + }, + + async frameHandler(): Promise { + throw new NotImplementedError('Not implemented'); + }, + + async refresh(this: never, req: Request, res: Response): Promise { + const { result } = await authenticator.authenticate( + { req }, + authenticatorCtx, + ); + + const { profile } = await profileTransform(result, resolverContext); + + const identity = await signInResolver( + { profile, result }, + resolverContext, + ); + + const response: ClientAuthResponse<{}> = { + profile, + providerInfo: {}, + backstageIdentity: prepareBackstageIdentityResponse(identity), + }; + + res.status(200).json(response); + }, + }; +} diff --git a/plugins/auth-node/src/proxy/index.ts b/plugins/auth-node/src/proxy/index.ts new file mode 100644 index 0000000000..907f8829e5 --- /dev/null +++ b/plugins/auth-node/src/proxy/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createProxyAuthenticator, type ProxyAuthenticator } from './types'; +export { createProxyAuthProviderFactory } from './createProxyAuthProviderFactory'; +export { + createProxyAuthRouteHandlers, + type ProxyAuthRouteHandlersOptions, +} from './createProxyRouteHandlers'; diff --git a/plugins/auth-node/src/proxy/types.ts b/plugins/auth-node/src/proxy/types.ts new file mode 100644 index 0000000000..969b022abb --- /dev/null +++ b/plugins/auth-node/src/proxy/types.ts @@ -0,0 +1,36 @@ +/* + * 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 { Config } from '@backstage/config'; +import { Request } from 'express'; +import { ProfileTransform } from '../types'; + +/** @public */ +export interface ProxyAuthenticator { + defaultProfileTransform: ProfileTransform; + initialize(ctx: { config: Config }): Promise; + authenticate( + options: { req: Request }, + ctx: TContext, + ): Promise<{ result: TResult }>; +} + +/** @public */ +export function createProxyAuthenticator( + authenticator: ProxyAuthenticator, +): ProxyAuthenticator { + return authenticator; +} diff --git a/plugins/auth-node/src/sign-in/commonSignInResolvers.ts b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts new file mode 100644 index 0000000000..5b5da07c73 --- /dev/null +++ b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createSignInResolverFactory } from './createSignInResolverFactory'; + +/** + * A collection of common sign-in resolvers that work with any auth provider. + * + * @public + */ +export namespace commonSignInResolvers { + /** + * A common sign-in resolver that looks up the user using their email address + * as email of the entity. + */ + export const emailMatchingUserEntityProfileEmail = + createSignInResolverFactory({ + create() { + return async (info, ctx) => { + const { profile } = info; + + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + + return ctx.signInWithCatalogUser({ + filter: { + 'spec.profile.email': profile.email, + }, + }); + }; + }, + }); + + /** + * A common sign-in resolver that looks up the user using the local part of + * their email address as the entity name. + */ + export const emailLocalPartMatchingUserEntityName = + createSignInResolverFactory({ + create() { + return async (info, ctx) => { + const { profile } = info; + + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + const [localPart] = profile.email.split('@'); + + return ctx.signInWithCatalogUser({ + entityRef: { name: localPart }, + }); + }; + }, + }); +} diff --git a/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts new file mode 100644 index 0000000000..0632b704c3 --- /dev/null +++ b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts @@ -0,0 +1,75 @@ +/* + * 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 { ZodSchema, ZodTypeDef } from 'zod'; +import { SignInResolver } from '../types'; +import zodToJsonSchema from 'zod-to-json-schema'; +import { JsonObject } from '@backstage/types'; +import { InputError } from '@backstage/errors'; + +/** @public */ +export interface SignInResolverFactory { + ( + ...options: undefined extends TOptions + ? [options?: TOptions] + : [options: TOptions] + ): SignInResolver; + optionsJsonSchema?: JsonObject; +} + +/** @public */ +export interface SignInResolverFactoryOptions< + TAuthResult, + TOptionsOutput, + TOptionsInput, +> { + optionsSchema?: ZodSchema; + create(options: TOptionsOutput): SignInResolver; +} + +/** @public */ +export function createSignInResolverFactory< + TAuthResult, + TOptionsOutput, + TOptionsInput, +>( + options: SignInResolverFactoryOptions< + TAuthResult, + TOptionsOutput, + TOptionsInput + >, +): SignInResolverFactory { + const { optionsSchema } = options; + if (!optionsSchema) { + return (resolverOptions?: TOptionsInput) => { + if (resolverOptions) { + throw new InputError('sign-in resolver does not accept options'); + } + return options.create(undefined as TOptionsOutput); + }; + } + const factory = ( + ...[resolverOptions]: undefined extends TOptionsInput + ? [options?: TOptionsInput] + : [options: TOptionsInput] + ) => { + const parsedOptions = optionsSchema.parse(resolverOptions); + return options.create(parsedOptions); + }; + + factory.optionsJsonSchema = zodToJsonSchema(optionsSchema) as JsonObject; + return factory; +} diff --git a/plugins/auth-node/src/sign-in/index.ts b/plugins/auth-node/src/sign-in/index.ts new file mode 100644 index 0000000000..736393a762 --- /dev/null +++ b/plugins/auth-node/src/sign-in/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + createSignInResolverFactory, + type SignInResolverFactory, + type SignInResolverFactoryOptions, +} from './createSignInResolverFactory'; +export { + readDeclarativeSignInResolver, + type ReadDeclarativeSignInResolverOptions, +} from './readDeclarativeSignInResolver'; +export { commonSignInResolvers } from './commonSignInResolvers'; diff --git a/plugins/auth-node/src/sign-in/readDeclarativeSignInResolver.ts b/plugins/auth-node/src/sign-in/readDeclarativeSignInResolver.ts new file mode 100644 index 0000000000..82b4918326 --- /dev/null +++ b/plugins/auth-node/src/sign-in/readDeclarativeSignInResolver.ts @@ -0,0 +1,71 @@ +/* + * 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 { Config } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { SignInResolver } from '../types'; +import { SignInResolverFactory } from './createSignInResolverFactory'; + +/** @public */ +export interface ReadDeclarativeSignInResolverOptions { + config: Config; + signInResolverFactories: { + [name in string]: SignInResolverFactory; + }; +} + +/** @public */ +export function readDeclarativeSignInResolver( + options: ReadDeclarativeSignInResolverOptions, +): SignInResolver | undefined { + const resolvers = + options.config + .getOptionalConfigArray('signIn.resolvers') + ?.map(resolverConfig => { + const resolverName = resolverConfig.getString('resolver'); + if (!Object.hasOwn(options.signInResolverFactories, resolverName)) { + throw new Error( + `Sign-in resolver '${resolverName}' is not available`, + ); + } + const resolver = options.signInResolverFactories[resolverName]; + const { resolver: _ignored, ...resolverOptions } = + resolverConfig.get(); + + return resolver( + Object.keys(resolverOptions).length > 0 ? resolverOptions : undefined, + ); + }) ?? []; + + if (resolvers.length === 0) { + return undefined; + } + + return async (profile, context) => { + for (const resolver of resolvers) { + try { + return await resolver(profile, context); + } catch (error) { + if (error?.name === 'NotFoundError') { + continue; + } + throw error; + } + } + + throw new Error('Failed to sign-in, unable to resolve user identity'); + }; +} diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts index 9aec98a372..d4cb0ac48e 100644 --- a/plugins/auth-node/src/types.ts +++ b/plugins/auth-node/src/types.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -import { Request } from 'express'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { EntityFilterQuery } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; +import { Request, Response } from 'express'; /** * A representation of a successful Backstage sign-in. @@ -31,15 +36,6 @@ export interface BackstageSignInResult { token: string; } -/** - * Options to request the identity from a Backstage backend request - * - * @public - */ -export type IdentityApiGetIdentityRequest = { - request: Request; -}; - /** * Response object containing the {@link BackstageUserIdentity} and the token * from the authentication provider. @@ -47,6 +43,11 @@ export type IdentityApiGetIdentityRequest = { * @public */ export interface BackstageIdentityResponse extends BackstageSignInResult { + /** + * The number of seconds until the token expires. If not set, it can be assumed that the token does not expire. + */ + expiresInSeconds?: number; + /** * A plaintext description of the identity that is encapsulated within the token. */ @@ -76,3 +77,301 @@ export type BackstageUserIdentity = { */ ownershipEntityRefs: string[]; }; + +/** + * A query for a single user in the catalog. + * + * If `entityRef` is used, the default kind is `'User'`. + * + * If `annotations` are used, all annotations must be present and + * match the provided value exactly. Only entities of kind `'User'` will be considered. + * + * If `filter` are used they are passed on as they are to the `CatalogApi`. + * + * Regardless of the query method, the query must match exactly one entity + * in the catalog, or an error will be thrown. + * + * @public + */ +export type AuthResolverCatalogUserQuery = + | { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + } + | { + annotations: Record; + } + | { + filter: EntityFilterQuery; + }; + +/** + * Parameters used to issue new Backstage Tokens + * + * @public + */ +export type TokenParams = { + /** + * The claims that will be embedded within the token. At a minimum, this should include + * the subject claim, `sub`. It is common to also list entity ownership relations in the + * `ent` list. Additional claims may also be added at the developer's discretion except + * for the following list, which will be overwritten by the TokenIssuer: `iss`, `aud`, + * `iat`, and `exp`. The Backstage team also maintains the right add new claims in the future + * without listing the change as a "breaking change". + */ + claims: { + /** The token subject, i.e. User ID */ + sub: string; + /** A list of entity references that the user claims ownership through */ + ent?: string[]; + } & Record; +}; + +/** + * The context that is used for auth processing. + * + * @public + */ +export type AuthResolverContext = { + /** + * Issues a Backstage token using the provided parameters. + */ + issueToken(params: TokenParams): Promise<{ token: string }>; + + /** + * Finds a single user in the catalog using the provided query. + * + * See {@link AuthResolverCatalogUserQuery} for details. + */ + findCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise<{ entity: Entity }>; + + /** + * Finds a single user in the catalog using the provided query, and then + * issues an identity for that user using default ownership resolution. + * + * See {@link AuthResolverCatalogUserQuery} for details. + */ + signInWithCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise; +}; + +/** + * Any Auth provider needs to implement this interface which handles the routes in the + * auth backend. Any auth API requests from the frontend reaches these methods. + * + * The routes in the auth backend API are tied to these methods like below + * + * `/auth/[provider]/start -> start` + * `/auth/[provider]/handler/frame -> frameHandler` + * `/auth/[provider]/refresh -> refresh` + * `/auth/[provider]/logout -> logout` + * + * @public + */ +export interface AuthProviderRouteHandlers { + /** + * Handles the start route of the API. This initiates a sign in request with an auth provider. + * + * Request + * - scopes for the auth request (Optional) + * Response + * - redirect to the auth provider for the user to sign in or consent. + * - sets a nonce cookie and also pass the nonce as 'state' query parameter in the redirect request + */ + start(req: Request, res: Response): Promise; + + /** + * Once the user signs in or consents in the OAuth screen, the auth provider redirects to the + * callbackURL which is handled by this method. + * + * Request + * - to contain a nonce cookie and a 'state' query parameter + * Response + * - postMessage to the window with a payload that contains accessToken, expiryInSeconds?, idToken? and scope. + * - sets a refresh token cookie if the auth provider supports refresh tokens + */ + frameHandler(req: Request, res: Response): Promise; + + /** + * (Optional) If the auth provider supports refresh tokens then this method handles + * requests to get a new access token. + * + * Other types of providers may also use this method to implement its own logic to create new sessions + * upon request. For example, this can be used to create a new session for a provider that handles requests + * from an authenticating proxy. + * + * Request + * - to contain a refresh token cookie and scope (Optional) query parameter. + * Response + * - payload with accessToken, expiryInSeconds?, idToken?, scope and user profile information. + */ + refresh?(req: Request, res: Response): Promise; + + /** + * (Optional) Handles sign out requests + * + * Response + * - removes the refresh token cookie + */ + logout?(req: Request, res: Response): Promise; +} + +/** + * @public + * @deprecated Use top-level properties passed to `AuthProviderFactory` instead + */ +export type AuthProviderConfig = { + /** + * The protocol://domain[:port] where the app is hosted. This is used to construct the + * callbackURL to redirect to once the user signs in to the auth provider. + */ + baseUrl: string; + + /** + * The base URL of the app as provided by app.baseUrl + */ + appUrl: string; + + /** + * A function that is called to check whether an origin is allowed to receive the authentication result. + */ + isOriginAllowed: (origin: string) => boolean; + + /** + * The function used to resolve cookie configuration based on the auth provider options. + */ + cookieConfigurer?: CookieConfigurer; +}; + +/** @public */ +export type AuthProviderFactory = (options: { + providerId: string; + /** @deprecated Use top-level properties instead */ + globalConfig: AuthProviderConfig; + config: Config; + logger: LoggerService; + resolverContext: AuthResolverContext; + /** + * The protocol://domain[:port] where the app is hosted. This is used to construct the + * callbackURL to redirect to once the user signs in to the auth provider. + */ + baseUrl: string; + + /** + * The base URL of the app as provided by app.baseUrl + */ + appUrl: string; + + /** + * A function that is called to check whether an origin is allowed to receive the authentication result. + */ + isOriginAllowed: (origin: string) => boolean; + + /** + * The function used to resolve cookie configuration based on the auth provider options. + */ + cookieConfigurer?: CookieConfigurer; +}) => AuthProviderRouteHandlers; + +/** @public */ +export type ClientAuthResponse = { + providerInfo: TProviderInfo; + profile: ProfileInfo; + backstageIdentity?: BackstageIdentityResponse; +}; + +/** + * Type of sign in information context. Includes the profile information and + * authentication result which contains auth related information. + * + * @public + */ +export type SignInInfo = { + /** + * The simple profile passed down for use in the frontend. + */ + profile: ProfileInfo; + + /** + * The authentication result that was received from the authentication + * provider. + */ + result: TAuthResult; +}; + +/** + * Describes the function which handles the result of a successful + * authentication. Must return a valid {@link @backstage/plugin-auth-node#BackstageSignInResult}. + * + * @public + */ +export type SignInResolver = ( + info: SignInInfo, + context: AuthResolverContext, +) => Promise; + +/** + * Describes the function that transforms the result of a successful + * authentication into a {@link ProfileInfo} object. + * + * This function may optionally throw an error in order to reject authentication. + * + * @public + */ +export type ProfileTransform = ( + result: TResult, + context: AuthResolverContext, +) => Promise<{ profile: ProfileInfo }>; + +/** + * Used to display login information to user, i.e. sidebar popup. + * + * It is also temporarily used as the profile of the signed-in user's Backstage + * identity, but we want to replace that with data from identity and/org catalog + * service + * + * @public + */ +export type ProfileInfo = { + /** + * Email ID of the signed in user. + */ + email?: string; + /** + * Display name that can be presented to the signed in user. + */ + displayName?: string; + /** + * URL to an image that can be used as the display image or avatar of the + * signed in user. + */ + picture?: string; +}; + +/** + * The callback used to resolve the cookie configuration for auth providers that use cookies. + * @public + */ +export type CookieConfigurer = (ctx: { + /** ID of the auth provider that this configuration applies to */ + providerId: string; + /** The externally reachable base URL of the auth-backend plugin */ + baseUrl: string; + /** The configured callback URL of the auth provider */ + callbackUrl: string; + /** The origin URL of the app */ + appOrigin: string; +}) => { + domain: string; + path: string; + secure: boolean; + sameSite?: 'none' | 'lax' | 'strict'; +}; diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index fd9f614d75..b78a955bf5 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-azure-devops-backend +## 0.3.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-devops-common@0.3.0 + ## 0.3.27 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 6868de34fd..69a01e7928 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.3.27", + "version": "0.3.29-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index abf3c3e146..21e17ee554 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-azure-devops +## 0.3.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-devops-common@0.3.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.3.3 ### Patch Changes diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md index d083d00dba..2c916ec33b 100644 --- a/plugins/azure-devops/api-report.md +++ b/plugins/azure-devops/api-report.md @@ -14,8 +14,10 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { GitTag } from '@backstage/plugin-azure-devops-common'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { PullRequest } from '@backstage/plugin-azure-devops-common'; import { PullRequestOptions } from '@backstage/plugin-azure-devops-common'; +import { default as React_2 } from 'react'; import { Readme } from '@backstage/plugin-azure-devops-common'; import { ReadmeConfig } from '@backstage/plugin-azure-devops-common'; import { RepoBuild } from '@backstage/plugin-azure-devops-common'; @@ -163,14 +165,16 @@ export class AzureDevOpsClient implements AzureDevOpsApi { export const azureDevOpsPlugin: BackstagePlugin<{}, {}, {}>; // @public (undocumented) -export const AzurePullRequestsIcon: (props: SvgIconProps) => JSX.Element; +export const AzurePullRequestsIcon: ( + props: SvgIconProps, +) => React_2.JSX.Element; // @public (undocumented) export const AzurePullRequestsPage: (props: { projectName?: string | undefined; pollingInterval?: number | undefined; defaultColumnConfigs?: PullRequestColumnConfig[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export type BaseFilter = { @@ -223,22 +227,22 @@ export type CreatedByUserFilter = BaseFilter & ); // @public (undocumented) -export const EntityAzureGitTagsContent: () => JSX.Element; +export const EntityAzureGitTagsContent: () => JSX_2.Element; // @public (undocumented) export const EntityAzurePipelinesContent: (props: { defaultLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityAzurePullRequestsContent: (props: { defaultLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityAzureReadmeCard: (props: { maxHeight?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export type Filter = diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 96674d2cf2..9100f37091 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.3.3", + "version": "0.3.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "humanize-duration": "^3.27.0", "luxon": "^3.0.0", "react-use": "^17.2.4" @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/azure-sites-backend/CHANGELOG.md b/plugins/azure-sites-backend/CHANGELOG.md index 6e6a662283..039c8394e7 100644 --- a/plugins/azure-sites-backend/CHANGELOG.md +++ b/plugins/azure-sites-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-azure-sites-backend +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-sites-common@0.1.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json index 5b6f7ea83f..f7aa4e7d40 100644 --- a/plugins/azure-sites-backend/package.json +++ b/plugins/azure-sites-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites-backend", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-sites/CHANGELOG.md b/plugins/azure-sites/CHANGELOG.md index 6f043950c6..f0108ebd86 100644 --- a/plugins/azure-sites/CHANGELOG.md +++ b/plugins/azure-sites/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-azure-sites +## 0.1.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/azure-sites/api-report.md b/plugins/azure-sites/api-report.md index 38ca6a8fe2..bec89e17bc 100644 --- a/plugins/azure-sites/api-report.md +++ b/plugins/azure-sites/api-report.md @@ -13,6 +13,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -40,7 +42,7 @@ export class AzureSitesApiBackendClient implements AzureSitesApi { } // @public (undocumented) -export const AzureSitesOverviewWidget: () => JSX.Element; +export const AzureSitesOverviewWidget: () => React_2.JSX.Element; // @public (undocumented) export const azureSitesPlugin: BackstagePlugin< @@ -52,7 +54,7 @@ export const azureSitesPlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityAzureSitesOverviewWidget: () => JSX.Element; +export const EntityAzureSitesOverviewWidget: () => JSX_2.Element; // @public (undocumented) export const isAzureWebSiteNameAvailable: ( diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json index a4e50cccea..2829a80a91 100644 --- a/plugins/azure-sites/package.json +++ b/plugins/azure-sites/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -41,6 +41,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -59,7 +60,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index 2557b7469a..f6b19524a1 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-badges-backend +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + ## 0.2.3 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index f7acb9308f..df6f3d7d16 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.2.3", + "version": "0.2.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index 60e1ecc1be..c88dd603be 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-badges +## 0.2.47-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.2.45 ### Patch Changes diff --git a/plugins/badges/api-report.md b/plugins/badges/api-report.md index b051bbb452..e73a887316 100644 --- a/plugins/badges/api-report.md +++ b/plugins/badges/api-report.md @@ -6,6 +6,7 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export const badgesPlugin: BackstagePlugin<{}, {}, {}>; @@ -14,5 +15,5 @@ export const badgesPlugin: BackstagePlugin<{}, {}, {}>; export const EntityBadgesDialog: (props: { open: boolean; onClose?: (() => any) | undefined; -}) => JSX.Element; +}) => JSX_2.Element; ``` diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 9146f64de5..b42cefed69 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.45", + "version": "0.2.47-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index 2cd3f586c2..ebd73ebaf1 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-bazaar-backend +## 0.2.13-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + ## 0.2.11 ### Patch Changes diff --git a/plugins/bazaar-backend/migrations/20211014144054_init.js b/plugins/bazaar-backend/migrations/20211014144054_init.js index 503812225d..480771acb4 100644 --- a/plugins/bazaar-backend/migrations/20211014144054_init.js +++ b/plugins/bazaar-backend/migrations/20211014144054_init.js @@ -23,7 +23,7 @@ exports.up = async function up(knex) { await knex.schema.createTable('metadata', table => { table.comment('The table of Bazaar metadata'); table - .text('entity_ref') + .string('entity_ref') .notNullable() .unique() .comment('The ref of the entity'); @@ -49,7 +49,7 @@ exports.up = async function up(knex) { await knex.schema.createTable('members', table => { table.comment('The table of Bazaar members'); table - .text('entity_ref') + .string('entity_ref') .notNullable() .references('metadata.entity_ref') .onDelete('CASCADE') diff --git a/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js index 0953d5c2b1..ae2618fbef 100644 --- a/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js +++ b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js @@ -89,9 +89,11 @@ exports.up = async function up(knex) { await knex.schema.alterTable('members', table => { table .integer('item_id') + .unsigned() .references('metadata.id') .onDelete('CASCADE') .comment('Id of the associated item'); + table.dropForeign('entity_ref'); table.dropColumn('entity_ref'); }); } diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index d97267faf8..30778b2e3e 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.2.11", + "version": "0.2.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts b/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts index 0c78dbdd87..4e46581240 100644 --- a/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts +++ b/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts @@ -36,7 +36,7 @@ jest.setTimeout(60_000); describe('DatabaseHandler', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'], }); function createDatabaseManager( diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index b6bae0f5ed..06c5b21934 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-bazaar +## 0.2.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.2.12 ### Patch Changes diff --git a/plugins/bazaar/api-report.md b/plugins/bazaar/api-report.md index 11ce735bf5..282983b378 100644 --- a/plugins/bazaar/api-report.md +++ b/plugins/bazaar/api-report.md @@ -8,12 +8,14 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const BazaarOverviewCard: ( props: BazaarOverviewCardProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type BazaarOverviewCardProps = { @@ -24,7 +26,7 @@ export type BazaarOverviewCardProps = { }; // @public (undocumented) -export const BazaarPage: (props: BazaarPageProps) => JSX.Element; +export const BazaarPage: (props: BazaarPageProps) => JSX_2.Element; // @public (undocumented) export type BazaarPageProps = { @@ -44,7 +46,7 @@ export const bazaarPlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityBazaarInfoCard: () => JSX.Element | null; +export const EntityBazaarInfoCard: () => React_2.JSX.Element | null; // @public (undocumented) export const isBazaarAvailable: ( @@ -55,7 +57,7 @@ export const isBazaarAvailable: ( ) => Promise; // @public (undocumented) -export const SortView: (props: SortViewProps) => JSX.Element; +export const SortView: (props: SortViewProps) => React_2.JSX.Element; // @public (undocumented) export type SortViewProps = { diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 34d7490554..596cc1c8df 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.2.12", + "version": "0.2.15-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,6 +43,7 @@ "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/pickers": "^3.3.10", "@testing-library/jest-dom": "^5.10.1", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "material-ui-search-bar": "^1.0.0", "react-hook-form": "^7.13.0", @@ -56,8 +57,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/dev-utils": "workspace:^", - "@testing-library/jest-dom": "^5.10.1", - "@types/react": "^16.13.1 || ^17.0.0" + "@testing-library/jest-dom": "^5.10.1" }, "files": [ "dist" diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index f1a842f892..203ac91eda 100644 --- a/plugins/bitbucket-cloud-common/CHANGELOG.md +++ b/plugins/bitbucket-cloud-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-bitbucket-cloud-common +## 0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + ## 0.2.9 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index 2bc9fc3297..60d1de3c54 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitbucket-cloud-common", "description": "Common functionalities for bitbucket-cloud plugins", - "version": "0.2.9", + "version": "0.2.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index c44557a51c..253abe33ea 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-bitrise +## 0.1.50-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.48 ### Patch Changes diff --git a/plugins/bitrise/api-report.md b/plugins/bitrise/api-report.md index 01d10c61cf..7f2e44f04f 100644 --- a/plugins/bitrise/api-report.md +++ b/plugins/bitrise/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export const BITRISE_APP_ANNOTATION = 'bitrise.io/app'; @@ -15,7 +16,7 @@ export const BITRISE_APP_ANNOTATION = 'bitrise.io/app'; export const bitrisePlugin: BackstagePlugin<{}, {}, {}>; // @public (undocumented) -export const EntityBitriseContent: () => JSX.Element; +export const EntityBitriseContent: () => JSX_2.Element; // @public (undocumented) export const isBitriseAvailable: (entity: Entity) => boolean; diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 6d63cb6384..5b9737d170 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.48", + "version": "0.1.50-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@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", "luxon": "^3.0.0", "qs": "^6.9.6", @@ -58,7 +59,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "@types/recharts": "^1.8.15", "msw": "^1.0.0" }, diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index b2399d568f..e452a307cb 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + ## 0.2.3 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index e593fcf90a..97bcdbb103 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", "description": "A Backstage catalog backend module that helps integrate towards AWS", - "version": "0.2.3", + "version": "0.2.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts b/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts index c9fea8aa28..ad1e94cad6 100644 --- a/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts +++ b/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts @@ -14,15 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; @@ -40,12 +32,12 @@ describe('catalogModuleAwsS3EntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -66,16 +58,7 @@ describe('catalogModuleAwsS3EntityProvider', () => { features: [ catalogModuleAwsS3EntityProvider(), mockServices.rootConfig.factory({ data: config }), - createServiceFactory(() => ({ - service: coreServices.logger, - deps: {}, - factory: getVoidLogger, - }))(), - createServiceFactory(() => ({ - service: coreServices.scheduler, - deps: {}, - factory: () => scheduler, - }))(), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index 73b5489600..2404293f8a 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.21-next.0 + +### Patch Changes + +- 94f96508491d: Improve consistency of results from the `AzureDevOpsEntityProvider`. +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- 044b4f2fb1e3: Remove duplications from Azure search before committing the new locations to the catalog. +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + ## 0.1.19 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/catalog-info.yaml b/plugins/catalog-backend-module-azure/catalog-info.yaml new file mode 100644 index 0000000000..c1a74cac6f --- /dev/null +++ b/plugins/catalog-backend-module-azure/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-azure + title: '@backstage/plugin-catalog-backend-module-azure' + description: A Backstage catalog backend module that helps integrate towards Azure +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index eb6a3eadf5..896f801f38 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", "description": "A Backstage catalog backend module that helps integrate towards Azure", - "version": "0.1.19", + "version": "0.1.21-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts index f26208353a..493f0dda51 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -18,11 +18,40 @@ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { codeSearch, CodeSearchResponse } from './azure'; +import { + DefaultAzureDevOpsCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; describe('azure', () => { const server = setupServer(); setupRequestMockHandlers(server); + const createFixture = (host: string, token: string) => { + const azureConfig = { + host: host, + credentials: [ + { + personalAccessToken: token, + }, + ], + }; + const scmIntegrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [azureConfig], + }, + }), + ); + + return { + azureConfig: scmIntegrations.azure.byHost(host)?.config!, + credentialsProvider: + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(scmIntegrations), + }; + }; + describe('codeSearch', () => { it('returns empty when nothing is found', async () => { const response: CodeSearchResponse = { count: 0, results: [] }; @@ -34,6 +63,12 @@ describe('azure', () => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:* proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -42,9 +77,14 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', '', @@ -88,6 +128,12 @@ describe('azure', () => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:* proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -96,9 +142,14 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', '', @@ -132,6 +183,12 @@ describe('azure', () => { expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:backstage proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -140,9 +197,15 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); + await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', 'backstage', @@ -175,6 +238,12 @@ describe('azure', () => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:* proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -183,9 +252,15 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'azuredevops.mycompany.com', + 'ABC', + ); + await expect( codeSearch( - { host: 'azuredevops.mycompany.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', '', @@ -236,9 +311,15 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); + await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', 'backstage', diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.ts b/plugins/catalog-backend-module-azure/src/lib/azure.ts index 916d70a9fa..c8ff8dd739 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.ts @@ -16,8 +16,8 @@ import fetch from 'node-fetch'; import { + AzureDevOpsCredentialsProvider, AzureIntegrationConfig, - getAzureRequestOptions, } from '@backstage/integration'; export interface CodeSearchResponse { @@ -42,6 +42,7 @@ const PAGE_SIZE = 1000; // codeSearch returns all files that matches the given search path. export async function codeSearch( + credentialsProvider: AzureDevOpsCredentialsProvider, azureConfig: AzureIntegrationConfig, org: string, project: string, @@ -57,13 +58,24 @@ export async function codeSearch( let hasMorePages = true; do { + const credentials = await credentialsProvider.getCredentials({ + url: `https://${azureConfig.host}/${org}`, + }); + const response = await fetch(searchUrl, { - ...(await getAzureRequestOptions(azureConfig, { + headers: { + ...credentials?.headers, 'Content-Type': 'application/json', - })), + }, method: 'POST', body: JSON.stringify({ searchText: `path:${path} repo:${repo || '*'} proj:${project || '*'}`, + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: items.length, $top: PAGE_SIZE, }), diff --git a/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts b/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts index 3147a0871b..a20921fa3f 100644 --- a/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts +++ b/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; @@ -39,12 +32,12 @@ describe('catalogModuleAzureDevOpsEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -69,11 +62,7 @@ describe('catalogModuleAzureDevOpsEntityProvider', () => { catalogModuleAzureDevOpsEntityProvider(), mockServices.rootConfig.factory({ data: config }), mockServices.logger.factory(), - createServiceFactory(() => ({ - deps: {}, - service: coreServices.scheduler, - factory: async () => scheduler, - })), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts index 4a8e1d4537..36dd920621 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -158,6 +158,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', @@ -207,6 +208,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', @@ -248,6 +250,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', @@ -277,6 +280,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts index bad6067dc3..d9638e6cc7 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts @@ -16,6 +16,8 @@ import { Config } from '@backstage/config'; import { + AzureDevOpsCredentialsProvider, + DefaultAzureDevOpsCredentialsProvider, ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; @@ -46,6 +48,7 @@ import { codeSearch } from '../lib'; */ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; + private readonly credentialsProvider: AzureDevOpsCredentialsProvider; private readonly logger: Logger; static fromConfig(config: Config, options: { logger: Logger }) { @@ -63,6 +66,10 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { }) { this.integrations = options.integrations; this.logger = options.logger; + this.credentialsProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations( + options.integrations, + ); } getProcessorName(): string { @@ -93,6 +100,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { ); const files = await codeSearch( + this.credentialsProvider, azureConfig, org, project, diff --git a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts index 5abd23ce00..92398779c7 100644 --- a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts +++ b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts @@ -16,7 +16,12 @@ import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; import { Config } from '@backstage/config'; -import { AzureIntegration, ScmIntegrations } from '@backstage/integration'; +import { + AzureDevOpsCredentialsProvider, + AzureIntegration, + DefaultAzureDevOpsCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; import { EntityProvider, EntityProviderConnection, @@ -50,6 +55,9 @@ export class AzureDevOpsEntityProvider implements EntityProvider { }, ): AzureDevOpsEntityProvider[] { const providerConfigs = readAzureDevOpsConfigs(configRoot); + const scmIntegrations = ScmIntegrations.fromConfig(configRoot); + const credentialsProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(scmIntegrations); if (!options.schedule && !options.scheduler) { throw new Error('Either schedule or scheduler must be provided.'); @@ -79,6 +87,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { return new AzureDevOpsEntityProvider( providerConfig, integration, + credentialsProvider, options.logger, taskRunner, ); @@ -88,6 +97,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { private constructor( private readonly config: AzureDevOpsConfig, private readonly integration: AzureIntegration, + private readonly credentialsProvider: AzureDevOpsCredentialsProvider, logger: Logger, taskRunner: TaskRunner, ) { @@ -139,6 +149,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { logger.info('Discovering Azure DevOps catalog files'); const files = await codeSearch( + this.credentialsProvider, this.integration.config, this.config.organization, this.config.project, @@ -148,7 +159,10 @@ export class AzureDevOpsEntityProvider implements EntityProvider { logger.info(`Discovered ${files.length} catalog files`); - const locations = files.map(key => this.createLocationSpec(key)); + const targets = files.map(key => this.createObjectUrl(key)); + const locations = Array.from(new Set(targets)).map(key => + this.createLocationSpec(key), + ); await this.connection.applyMutation({ type: 'full', @@ -165,9 +179,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { ); } - private createLocationSpec(file: CodeSearchResultItem): LocationSpec { - const target = this.createObjectUrl(file); - + private createLocationSpec(target: string): LocationSpec { return { type: 'url', target: target, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index 7503ee898e..9da41d65f5 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-bitbucket-cloud-common@0.2.11-next.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + ## 0.1.15 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 42df54688b..8c699f8345 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", - "version": "0.1.15", + "version": "0.1.17-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.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 83769fc3f5..c771a40acc 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,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { startTestBackend, mockServices } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; @@ -46,12 +39,12 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); await startTestBackend({ extensionPoints: [ @@ -75,11 +68,7 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { }, }, }), - createServiceFactory({ - service: coreServices.scheduler, - deps: {}, - factory: async () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index 1b5a65f254..7614fbe260 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + ## 0.1.13 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/catalog-info.yaml b/plugins/catalog-backend-module-bitbucket-server/catalog-info.yaml new file mode 100644 index 0000000000..47c49530a0 --- /dev/null +++ b/plugins/catalog-backend-module-bitbucket-server/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-bitbucket-server + title: '@backstage/plugin-catalog-backend-module-bitbucket-server' +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 7e488e31ff..fc17120b8d 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.1.13", + "version": "0.1.15-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts index ae4a636008..ab92e24fd7 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { catalogModuleBitbucketServerEntityProvider } from './catalogModuleBitbucketServerEntityProvider'; @@ -39,12 +32,12 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -73,11 +66,7 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { catalogModuleBitbucketServerEntityProvider(), mockServices.rootConfig.factory({ data: config }), mockServices.logger.factory(), - createServiceFactory({ - service: coreServices.scheduler, - deps: {}, - factory: async () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md index ef448f6b20..ff995a2f37 100644 --- a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-bitbucket +## 0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.11-next.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + ## 0.2.15 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket/catalog-info.yaml b/plugins/catalog-backend-module-bitbucket/catalog-info.yaml new file mode 100644 index 0000000000..a384181766 --- /dev/null +++ b/plugins/catalog-backend-module-bitbucket/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-bitbucket + title: '@backstage/plugin-catalog-backend-module-bitbucket' + description: A Backstage catalog backend module that helps integrate towards Bitbucket +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-bitbucket/package.json b/plugins/catalog-backend-module-bitbucket/package.json index 45ca64dc71..3679de4cc6 100644 --- a/plugins/catalog-backend-module-bitbucket/package.json +++ b/plugins/catalog-backend-module-bitbucket/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket", - "version": "0.2.15", + "version": "0.2.17-next.0", "deprecated": true, "main": "src/index.ts", "types": "src/index.ts", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index c30f91fa61..ea0f51b935 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + ## 0.1.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-gcp/catalog-info.yaml b/plugins/catalog-backend-module-gcp/catalog-info.yaml new file mode 100644 index 0000000000..2a8b83b3ee --- /dev/null +++ b/plugins/catalog-backend-module-gcp/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-gcp + title: '@backstage/plugin-catalog-backend-module-gcp' + description: A Backstage catalog backend module that helps integrate towards GCP +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index dcff6662dd..b0af660a3f 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", "description": "A Backstage catalog backend module that helps integrate towards GCP", - "version": "0.1.0", + "version": "0.1.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index 3bb53b864b..02676aac64 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + ## 0.1.16 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/catalog-info.yaml b/plugins/catalog-backend-module-gerrit/catalog-info.yaml new file mode 100644 index 0000000000..9f75a840aa --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-gerrit + title: '@backstage/plugin-catalog-backend-module-gerrit' +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 99e9ed5e12..e0573c84cb 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.1.16", + "version": "0.1.18-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts b/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts index 9807814f61..4afcd8e2d4 100644 --- a/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; @@ -39,12 +32,12 @@ describe('catalogModuleGerritEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -79,11 +72,7 @@ describe('catalogModuleGerritEntityProvider', () => { catalogModuleGerritEntityProvider(), mockServices.rootConfig.factory({ data: config }), mockServices.logger.factory(), - createServiceFactory({ - service: coreServices.scheduler, - deps: {}, - factory: async () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index 4d14c2ce5a..e57b69b91f 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-backend-module-github +## 0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + ## 0.3.3 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/catalog-info.yaml b/plugins/catalog-backend-module-github/catalog-info.yaml new file mode 100644 index 0000000000..2b1b9a9553 --- /dev/null +++ b/plugins/catalog-backend-module-github/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-github + title: '@backstage/plugin-catalog-backend-module-github' + description: A Backstage catalog backend module that helps integrate towards GitHub +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index 101c0f54f0..581b277f02 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-github", "description": "A Backstage catalog backend module that helps integrate towards GitHub", - "version": "0.3.3", + "version": "0.3.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts index 9e36966eec..171a641fc7 100644 --- a/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts @@ -14,19 +14,12 @@ * limitations under the License. */ -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; import { catalogModuleGithubEntityProvider } from './catalogModuleGithubEntityProvider'; import { GithubEntityProvider } from '../providers/GithubEntityProvider'; -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; describe('catalogModuleGithubEntityProvider', () => { it('should register provider at the catalog extension point', async () => { @@ -39,12 +32,12 @@ describe('catalogModuleGithubEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -65,11 +58,7 @@ describe('catalogModuleGithubEntityProvider', () => { features: [ catalogModuleGithubEntityProvider(), mockServices.rootConfig.factory({ data: config }), - createServiceFactory({ - service: coreServices.scheduler, - deps: {}, - factory: async () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 0f860bb050..39b771852d 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + ## 0.2.4 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/catalog-info.yaml b/plugins/catalog-backend-module-gitlab/catalog-info.yaml new file mode 100644 index 0000000000..41968a1c34 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-gitlab + title: '@backstage/plugin-catalog-backend-module-gitlab' + description: A Backstage catalog backend module that helps integrate towards GitLab +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 3f31ffdf1d..9d316d523e 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", "description": "A Backstage catalog backend module that helps integrate towards GitLab", - "version": "0.2.4", + "version": "0.2.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts index 22759cafa0..46cfe403e8 100644 --- a/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; @@ -39,12 +32,12 @@ describe('catalogModuleGitlabDiscoveryEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { integrations: { @@ -78,11 +71,7 @@ describe('catalogModuleGitlabDiscoveryEntityProvider', () => { catalogModuleGitlabDiscoveryEntityProvider(), mockServices.rootConfig.factory({ data: config }), mockServices.logger.factory(), - createServiceFactory({ - deps: {}, - service: coreServices.scheduler, - factory: async () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index 7fa5ac3c11..d40a699072 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-permission-common@0.7.7 + ## 0.4.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/catalog-info.yaml b/plugins/catalog-backend-module-incremental-ingestion/catalog-info.yaml new file mode 100644 index 0000000000..78c1b33d19 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-incremental-ingestion + title: '@backstage/plugin-catalog-backend-module-incremental-ingestion' + description: An entity provider for streaming large asset sources into the catalog +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index 30a64c20b2..6c3601f190 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", "description": "An entity provider for streaming large asset sources into the catalog", - "version": "0.4.1", + "version": "0.4.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts index 0d910f664d..1f318c2590 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts @@ -24,7 +24,7 @@ import { WrapperProviders } from './WrapperProviders'; describe('WrapperProviders', () => { const applyDatabaseMigrations = jest.fn(); const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'], }); const config = new ConfigReader({}); const logger = getVoidLogger(); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts index ed0c776fa8..7d76ddf480 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts @@ -14,12 +14,8 @@ * limitations under the License. */ -import { - coreServices, - createBackendModule, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { startTestBackend } from '@backstage/backend-test-utils'; +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { IncrementalEntityProvider } from '../types'; import { @@ -40,18 +36,15 @@ describe('catalogModuleIncrementalIngestionEntityProvider', () => { }; const addEntityProvider = jest.fn(); - const httpRouterUse = jest.fn(); + + const httpRouterMock = mockServices.httpRouter.mock(); await startTestBackend({ extensionPoints: [ [catalogProcessingExtensionPoint, { addEntityProvider }], ], features: [ - createServiceFactory({ - service: coreServices.httpRouter, - deps: {}, - factory: () => ({ use: httpRouterUse }), - }), + httpRouterMock.factory, catalogModuleIncrementalIngestionEntityProvider(), createBackendModule({ pluginId: 'catalog', @@ -79,6 +72,6 @@ describe('catalogModuleIncrementalIngestionEntityProvider', () => { expect(addEntityProvider.mock.calls[0][0].getProviderName()).toBe( 'provider1', ); - expect(httpRouterUse).toHaveBeenCalledTimes(1); + expect(httpRouterMock.use).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 9db8a14c9f..9e4c2fca9e 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.5.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + ## 0.5.15 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/catalog-info.yaml b/plugins/catalog-backend-module-ldap/catalog-info.yaml new file mode 100644 index 0000000000..b3032ca72e --- /dev/null +++ b/plugins/catalog-backend-module-ldap/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-ldap + title: '@backstage/plugin-catalog-backend-module-ldap' + description: A Backstage catalog backend module that helps integrate towards LDAP +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 8d3a6277c3..d4bf2beb8e 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend module that helps integrate towards LDAP", - "version": "0.5.15", + "version": "0.5.17-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 383ceffe0e..06e2ef66e4 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + ## 0.5.7 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 5d2afe69bf..93b10ca444 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", - "version": "0.5.7", + "version": "0.5.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts index 96fec41cbe..d36995b1c8 100644 --- a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; @@ -39,12 +32,12 @@ describe('catalogModuleMicrosoftGraphOrgEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -70,11 +63,7 @@ describe('catalogModuleMicrosoftGraphOrgEntityProvider', () => { features: [ catalogModuleMicrosoftGraphOrgEntityProvider(), mockServices.rootConfig.factory({ data: config }), - createServiceFactory(() => ({ - deps: {}, - service: coreServices.scheduler, - factory: async () => scheduler, - })), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 7a7bf51087..e49cd83d86 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + ## 0.1.14 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/catalog-info.yaml b/plugins/catalog-backend-module-openapi/catalog-info.yaml new file mode 100644 index 0000000000..507bf555db --- /dev/null +++ b/plugins/catalog-backend-module-openapi/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-openapi + title: '@backstage/plugin-catalog-backend-module-openapi' + description: A Backstage catalog backend module that helps with OpenAPI specifications +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index f9c5fe88a1..df872f7703 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", - "version": "0.1.14", + "version": "0.1.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index 31fbe07360..342588b96b 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + ## 0.1.5 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index 9cd16fd3c3..06465c4b69 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", - "version": "0.1.5", + "version": "0.1.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts index 3ea54d8c8f..f931b2c741 100644 --- a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { catalogModulePuppetDbEntityProvider } from './catalogModulePuppetDbEntityProvider'; @@ -38,12 +31,12 @@ describe('catalogModulePuppetDbEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -64,11 +57,7 @@ describe('catalogModulePuppetDbEntityProvider', () => { features: [ catalogModulePuppetDbEntityProvider(), mockServices.rootConfig.factory({ data: config }), - createServiceFactory(() => ({ - deps: {}, - service: coreServices.scheduler, - factory: async () => scheduler, - })), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index 96228acefd..2e7340ad5e 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + ## 0.2.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-unprocessed/catalog-info.yaml b/plugins/catalog-backend-module-unprocessed/catalog-info.yaml new file mode 100644 index 0000000000..a889859494 --- /dev/null +++ b/plugins/catalog-backend-module-unprocessed/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-unprocessed + title: '@backstage/plugin-catalog-backend-module-unprocessed' + description: Backstage Catalog module to view unprocessed entities +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index 8f303b9667..90a0999197 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", "description": "Backstage Catalog module to view unprocessed entities", - "version": "0.2.0", + "version": "0.2.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index e1fd4e43ab..c75b2fd604 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-catalog-backend +## 1.12.2-next.0 + +### Patch Changes + +- 149361e81622: Fix to the `limit` parameter on entity queries. +- 0198aa596fd9: Fixed a link to the frontend Backstage plugin that had pointed to itself. +- 41d1b2d628ea: Fix OpenAPI schema for the facets endpoint +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-search-backend-module-catalog@0.1.6-next.0 + - @backstage/plugin-search-common@1.2.5 + ## 1.12.0 ### Minor Changes diff --git a/plugins/catalog-backend/README.md b/plugins/catalog-backend/README.md index 1e2bce3142..cd921499d3 100644 --- a/plugins/catalog-backend/README.md +++ b/plugins/catalog-backend/README.md @@ -86,5 +86,5 @@ some example entities. ## Links -- [catalog](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend) +- [catalog](https://github.com/backstage/backstage/tree/master/plugins/catalog) is the frontend interface for this plugin. diff --git a/plugins/catalog-backend/catalog-info.yaml b/plugins/catalog-backend/catalog-info.yaml new file mode 100644 index 0000000000..b94d3b458a --- /dev/null +++ b/plugins/catalog-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend + title: '@backstage/plugin-catalog-backend' + description: The Backstage backend plugin that provides the Backstage catalog +spec: + lifecycle: production + type: backstage-backend-plugin + owner: catalog-maintainers diff --git a/plugins/catalog-backend/migrations/20210302150147_refresh_state.js b/plugins/catalog-backend/migrations/20210302150147_refresh_state.js index 1c8081c0ef..f600667727 100644 --- a/plugins/catalog-backend/migrations/20210302150147_refresh_state.js +++ b/plugins/catalog-backend/migrations/20210302150147_refresh_state.js @@ -37,7 +37,7 @@ exports.up = async function up(knex) { .notNullable() .comment('The unprocessed entity (in original form) as JSON'); table - .text('processed_entity') + .text('processed_entity', 'longtext') .nullable() .comment('The processed entity (not yet stitched) as JSON'); table @@ -83,7 +83,7 @@ exports.up = async function up(knex) { .notNullable() .comment('Random value representing a unique stitch attempt ticket'); table - .text('final_entity') + .text('final_entity', 'longtext') .nullable() .comment('The JSON encoded final entity'); table.index('entity_id', 'final_entities_entity_id_idx'); diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 0082898ffc..5a92b97b35 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "1.12.0", + "version": "1.12.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts index 4959a33eef..61a062bc4a 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts @@ -138,6 +138,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { type, }), ); + await tx.batchInsert( 'relations', this.deduplicateRelations(relationRows), diff --git a/plugins/catalog-backend/src/schema/openapi.generated.ts b/plugins/catalog-backend/src/schema/openapi.generated.ts index 7428e5ace2..3a56893b48 100644 --- a/plugins/catalog-backend/src/schema/openapi.generated.ts +++ b/plugins/catalog-backend/src/schema/openapi.generated.ts @@ -135,7 +135,7 @@ export const spec = { required: false, schema: { type: 'integer', - minimum: 1, + minimum: 0, }, }, orderField: { @@ -1087,7 +1087,10 @@ export const spec = { name: 'facet', required: true, schema: { - type: 'string', + type: 'array', + items: { + type: 'string', + }, }, }, { diff --git a/plugins/catalog-backend/src/schema/openapi.yaml b/plugins/catalog-backend/src/schema/openapi.yaml index 34e21adffd..e0b60b1a98 100644 --- a/plugins/catalog-backend/src/schema/openapi.yaml +++ b/plugins/catalog-backend/src/schema/openapi.yaml @@ -90,7 +90,7 @@ components: required: false schema: type: integer - minimum: 1 + minimum: 0 orderField: name: orderField in: query @@ -819,7 +819,9 @@ paths: name: facet required: true schema: - type: string + type: array + items: + type: string - $ref: '#/components/parameters/filter' /locations: post: diff --git a/plugins/catalog-common/catalog-info.yaml b/plugins/catalog-common/catalog-info.yaml new file mode 100644 index 0000000000..85ee85917b --- /dev/null +++ b/plugins/catalog-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-common + title: '@backstage/plugin-catalog-common' + description: Common functionalities for the catalog plugin +spec: + lifecycle: production + type: backstage-common-library + owner: catalog-maintainers diff --git a/plugins/catalog-customized/CHANGELOG.md b/plugins/catalog-customized/CHANGELOG.md index 2c98b37be3..613736d83c 100644 --- a/plugins/catalog-customized/CHANGELOG.md +++ b/plugins/catalog-customized/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/plugin-catalog-customized +## 0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.0.13 ### Patch Changes diff --git a/plugins/catalog-customized/catalog-info.yaml b/plugins/catalog-customized/catalog-info.yaml new file mode 100644 index 0000000000..1a6d0e0a00 --- /dev/null +++ b/plugins/catalog-customized/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: internal-plugin-catalog-customized + title: '@internal/plugin-catalog-customized' + description: >- + The internal Backstage Customizable plugin for browsing the Backstage + catalog +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: catalog-maintainers diff --git a/plugins/catalog-customized/package.json b/plugins/catalog-customized/package.json index bb298bf0ae..084a5b4126 100644 --- a/plugins/catalog-customized/package.json +++ b/plugins/catalog-customized/package.json @@ -1,7 +1,7 @@ { "name": "@internal/plugin-catalog-customized", "description": "The internal Backstage Customizable plugin for browsing the Backstage catalog", - "version": "0.0.13", + "version": "0.0.14-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index f36eadb464..b0c83297c1 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-graph +## 0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.2.33 ### Patch Changes diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index 80578db6ed..ce56245c49 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -11,8 +11,10 @@ import { DependencyGraphTypes } from '@backstage/core-components'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; import { JsonObject } from '@backstage/types'; +import { JSX as JSX_2 } from 'react'; import { MouseEvent as MouseEvent_2 } from 'react'; import { MouseEventHandler } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -35,7 +37,7 @@ export const CatalogGraphPage: ( } | undefined; } & Partial, -) => JSX.Element; +) => JSX_2.Element; // @public export const catalogGraphPlugin: BackstagePlugin< @@ -73,7 +75,7 @@ export const EntityCatalogGraphCard: ( height?: number | undefined; title?: string | undefined; }, -) => JSX.Element; +) => JSX_2.Element; // @public export type EntityEdge = DependencyGraphTypes.DependencyEdge; @@ -102,7 +104,7 @@ export type EntityNodeData = { // @public export const EntityRelationsGraph: ( props: EntityRelationsGraphProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type EntityRelationsGraphProps = { diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index b1a1abe6ba..9759a918f3 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.33", + "version": "0.2.35-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-graphql/catalog-info.yaml b/plugins/catalog-graphql/catalog-info.yaml new file mode 100644 index 0000000000..202acc8c95 --- /dev/null +++ b/plugins/catalog-graphql/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-graphql + title: '@backstage/plugin-catalog-graphql' + description: An experimental Backstage catalog GraphQL module +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: catalog-maintainers diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 8a44026997..d876ee979c 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-import +## 0.9.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.9.11 ### Patch Changes diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md index b4c8e854aa..f507967d37 100644 --- a/plugins/catalog-import/api-report.md +++ b/plugins/catalog-import/api-report.md @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { FieldErrors } from 'react-hook-form'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmAuthApi } from '@backstage/integration-react'; @@ -46,7 +47,7 @@ export type AnalyzeResult = // @public export const AutocompleteTextField: ( props: AutocompleteTextFieldProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface AutocompleteTextFieldProps { @@ -126,7 +127,7 @@ export class CatalogImportClient implements CatalogImportApi { } // @public -export const CatalogImportPage: () => JSX.Element; +export const CatalogImportPage: () => JSX_2.Element; // @public const catalogImportPlugin: BackstagePlugin< @@ -148,12 +149,12 @@ export function defaultGenerateStepper( ): StepperProvider; // @public -export const DefaultImportPage: () => JSX.Element; +export const DefaultImportPage: () => React_2.JSX.Element; // @public export const EntityListComponent: ( props: EntityListComponentProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface EntityListComponentProps { @@ -182,7 +183,9 @@ export type ImportFlows = | 'no-location'; // @public -export const ImportInfoCard: (props: ImportInfoCardProps) => JSX.Element; +export const ImportInfoCard: ( + props: ImportInfoCardProps, +) => React_2.JSX.Element; // @public export interface ImportInfoCardProps { @@ -205,7 +208,7 @@ export type ImportState = State & { }; // @public -export const ImportStepper: (props: ImportStepperProps) => JSX.Element; +export const ImportStepper: (props: ImportStepperProps) => React_2.JSX.Element; // @public export interface ImportStepperProps { @@ -223,7 +226,7 @@ export interface ImportStepperProps { // @public export const PreparePullRequestForm: >( props: PreparePullRequestFormProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type PreparePullRequestFormProps< @@ -266,7 +269,7 @@ export type PrepareResult = // @public export const PreviewCatalogInfoComponent: ( props: PreviewCatalogInfoComponentProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface PreviewCatalogInfoComponentProps { @@ -284,7 +287,7 @@ export interface PreviewCatalogInfoComponentProps { // @public export const PreviewPullRequestComponent: ( props: PreviewPullRequestComponentProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface PreviewPullRequestComponentProps { @@ -302,7 +305,7 @@ export interface PreviewPullRequestComponentProps { // @public export const StepInitAnalyzeUrl: ( props: StepInitAnalyzeUrlProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface StepInitAnalyzeUrlProps { @@ -326,7 +329,7 @@ export interface StepInitAnalyzeUrlProps { // @public export const StepPrepareCreatePullRequest: ( props: StepPrepareCreatePullRequestProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface StepPrepareCreatePullRequestProps { diff --git a/plugins/catalog-import/catalog-info.yaml b/plugins/catalog-import/catalog-info.yaml new file mode 100644 index 0000000000..763dec9c45 --- /dev/null +++ b/plugins/catalog-import/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-import + title: '@backstage/plugin-catalog-import' + description: A Backstage plugin the helps you import entities into your catalog +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: catalog-maintainers diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index fe9ef62251..b51b2dacdc 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.9.11", + "version": "0.9.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index 4e7b43ae7c..a40acf6a9a 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-node +## 1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + ## 1.4.1 ### Patch Changes diff --git a/plugins/catalog-node/catalog-info.yaml b/plugins/catalog-node/catalog-info.yaml new file mode 100644 index 0000000000..0dd6686eba --- /dev/null +++ b/plugins/catalog-node/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-node + title: '@backstage/plugin-catalog-node' + description: The plugin-catalog-node module for @backstage/plugin-catalog-backend +spec: + lifecycle: production + type: backstage-node-library + owner: catalog-maintainers diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 7c6e21d62c..3ab60a139a 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-node", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", - "version": "1.4.1", + "version": "1.4.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 953438ae7d..d848267c98 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-react +## 1.8.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + ## 1.8.1 ### Patch Changes diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index b5ab8b6c7f..0891875e3c 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { ApiRef } from '@backstage/core-plugin-api'; import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; @@ -30,7 +28,7 @@ import { TableOptions } from '@backstage/core-components'; // @public export const AsyncEntityProvider: ( props: AsyncEntityProviderProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface AsyncEntityProviderProps { @@ -62,15 +60,15 @@ export const catalogApiRef: ApiRef; // @public (undocumented) export const CatalogFilterLayout: { - (props: { children: React_2.ReactNode }): JSX.Element; + (props: { children: React_2.ReactNode }): React_2.JSX.Element; Filters: (props: { children: React_2.ReactNode; options?: { drawerBreakpoint?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number; drawerAnchor?: 'left' | 'right' | 'top' | 'bottom'; }; - }) => JSX.Element; - Content: (props: { children: React_2.ReactNode }) => JSX.Element; + }) => React_2.JSX.Element; + Content: (props: { children: React_2.ReactNode }) => React_2.JSX.Element; }; // @public (undocumented) @@ -177,7 +175,7 @@ export class EntityKindFilter implements EntityFilter { // @public (undocumented) export const EntityKindPicker: ( props: EntityKindPickerProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public export interface EntityKindPickerProps { @@ -202,7 +200,7 @@ export class EntityLifecycleFilter implements EntityFilter { // @public (undocumented) export const EntityLifecyclePicker: (props: { initialFilter?: string[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export const EntityListContext: React_2.Context< @@ -229,7 +227,7 @@ export type EntityListContextProps< // @public export const EntityListProvider: ( props: PropsWithChildren<{}>, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type EntityLoadingStatus = { @@ -251,7 +249,7 @@ export class EntityNamespaceFilter implements EntityFilter { } // @public (undocumented) -export const EntityNamespacePicker: () => JSX.Element; +export const EntityNamespacePicker: () => React_2.JSX.Element; // @public export class EntityOrphanFilter implements EntityFilter { @@ -275,7 +273,7 @@ export class EntityOwnerFilter implements EntityFilter { // @public (undocumented) export const EntityOwnerPicker: ( props?: EntityOwnerPickerProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public (undocumented) export type EntityOwnerPickerProps = { @@ -285,7 +283,7 @@ export type EntityOwnerPickerProps = { // @public export const EntityPeekAheadPopover: ( props: EntityPeekAheadPopoverProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type EntityPeekAheadPopoverProps = PropsWithChildren<{ @@ -294,10 +292,12 @@ export type EntityPeekAheadPopoverProps = PropsWithChildren<{ }>; // @public (undocumented) -export const EntityProcessingStatusPicker: () => JSX.Element; +export const EntityProcessingStatusPicker: () => React_2.JSX.Element; // @public -export const EntityProvider: (props: EntityProviderProps) => JSX.Element; +export const EntityProvider: ( + props: EntityProviderProps, +) => React_2.JSX.Element; // @public export interface EntityProviderProps { @@ -321,7 +321,7 @@ export type EntityRefLinkProps = { // @public export function EntityRefLinks< TRef extends string | CompoundEntityRef | Entity, ->(props: EntityRefLinksProps): JSX.Element; +>(props: EntityRefLinksProps): React_2.JSX.Element; // @public export type EntityRefLinksProps< @@ -357,7 +357,7 @@ export const entityRouteRef: RouteRef<{ }>; // @public -export const EntitySearchBar: () => JSX.Element; +export const EntitySearchBar: () => React_2.JSX.Element; // @public (undocumented) export type EntitySourceLocation = { @@ -367,7 +367,7 @@ export type EntitySourceLocation = { // @public export const EntityTable: { - (props: EntityTableProps): JSX.Element; + (props: EntityTableProps): React_2.JSX.Element; columns: Readonly<{ createEntityRefColumn(options: { defaultKind?: string | undefined; @@ -421,7 +421,9 @@ export class EntityTagFilter implements EntityFilter { } // @public (undocumented) -export const EntityTagPicker: (props: EntityTagPickerProps) => JSX.Element; +export const EntityTagPicker: ( + props: EntityTagPickerProps, +) => React_2.JSX.Element; // @public (undocumented) export type EntityTagPickerProps = { @@ -453,7 +455,7 @@ export class EntityTypeFilter implements EntityFilter { // @public (undocumented) export const EntityTypePicker: ( props: EntityTypePickerProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public export interface EntityTypePickerProps { @@ -464,7 +466,9 @@ export interface EntityTypePickerProps { } // @public -export const FavoriteEntity: (props: FavoriteEntityProps) => JSX.Element; +export const FavoriteEntity: ( + props: FavoriteEntityProps, +) => React_2.JSX.Element; // @public (undocumented) export type FavoriteEntityProps = ComponentProps & { @@ -500,7 +504,7 @@ export function InspectEntityDialog(props: { open: boolean; entity: Entity; onClose: () => void; -}): JSX.Element | null; +}): React_2.JSX.Element | null; // @public (undocumented) export function MockEntityListContextProvider< @@ -509,7 +513,7 @@ export function MockEntityListContextProvider< props: PropsWithChildren<{ value?: Partial>; }>, -): JSX.Element; +): React_2.JSX.Element; // @public export class MockStarredEntitiesApi implements StarredEntitiesApi { @@ -531,7 +535,7 @@ export const starredEntitiesApiRef: ApiRef; // @public (undocumented) export const UnregisterEntityDialog: ( props: UnregisterEntityDialogProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type UnregisterEntityDialogProps = { @@ -607,7 +611,9 @@ export class UserListFilter implements EntityFilter { export type UserListFilterKind = 'owned' | 'starred' | 'all'; // @public (undocumented) -export const UserListPicker: (props: UserListPickerProps) => JSX.Element; +export const UserListPicker: ( + props: UserListPickerProps, +) => React_2.JSX.Element; // @public (undocumented) export type UserListPickerProps = { diff --git a/plugins/catalog-react/catalog-info.yaml b/plugins/catalog-react/catalog-info.yaml new file mode 100644 index 0000000000..0fcfcbfb8e --- /dev/null +++ b/plugins/catalog-react/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-react + title: '@backstage/plugin-catalog-react' + description: >- + A frontend library that helps other Backstage plugins interact with the + catalog +spec: + lifecycle: production + type: backstage-web-library + owner: catalog-maintainers diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 9853935353..2492ce318f 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "1.8.1", + "version": "1.8.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-unprocessed-entities/CHANGELOG.md b/plugins/catalog-unprocessed-entities/CHANGELOG.md index 3efc8ad3d4..f0479a21ef 100644 --- a/plugins/catalog-unprocessed-entities/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-unprocessed-entities +## 0.1.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + ## 0.1.2 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities/api-report.md b/plugins/catalog-unprocessed-entities/api-report.md index 0ccf9716f2..3ded42785a 100644 --- a/plugins/catalog-unprocessed-entities/api-report.md +++ b/plugins/catalog-unprocessed-entities/api-report.md @@ -8,6 +8,8 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -25,7 +27,7 @@ export type CatalogUnprocessedEntitiesApiResponse = { }; // @public -export const CatalogUnprocessedEntitiesPage: () => JSX.Element; +export const CatalogUnprocessedEntitiesPage: () => JSX_2.Element; // @public export const catalogUnprocessedEntitiesPlugin: BackstagePlugin< @@ -37,7 +39,7 @@ export const catalogUnprocessedEntitiesPlugin: BackstagePlugin< >; // @public (undocumented) -export const UnprocessedEntitiesContent: () => JSX.Element; +export const UnprocessedEntitiesContent: () => React_2.JSX.Element; // @public export type UnprocessedEntity = { diff --git a/plugins/catalog-unprocessed-entities/catalog-info.yaml b/plugins/catalog-unprocessed-entities/catalog-info.yaml new file mode 100644 index 0000000000..8be164f111 --- /dev/null +++ b/plugins/catalog-unprocessed-entities/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-unprocessed-entities + title: '@backstage/plugin-catalog-unprocessed-entities' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: catalog-maintainers diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index 2a0d191b62..8f7aaa3c23 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities", - "version": "0.1.2", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,6 +36,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.60", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 055f2971a2..edbe72d74b 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-catalog +## 1.12.4-next.0 + +### Patch Changes + +- 832eef72485b: Added title to props for `HasCards`. +- 163a41035e42: Fixed an issue where `EntitySwitch` was preventing the display of entity errors. +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + ## 1.12.1 ### Patch Changes diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 3e33b16d8b..c8956dcbe8 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -15,6 +15,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { Observable } from '@backstage/types'; import { Overrides } from '@material-ui/core/styles/overrides'; import { default as React_2 } from 'react'; @@ -39,7 +40,7 @@ export interface AboutCardProps { } // @public (undocumented) -export function AboutContent(props: AboutContentProps): JSX.Element; +export function AboutContent(props: AboutContentProps): React_2.JSX.Element; // @public export interface AboutContentProps { @@ -48,7 +49,7 @@ export interface AboutContentProps { } // @public (undocumented) -export function AboutField(props: AboutFieldProps): JSX.Element; +export function AboutField(props: AboutFieldProps): React_2.JSX.Element; // @public export interface AboutFieldProps { @@ -79,7 +80,9 @@ export const CatalogEntityPage: () => JSX.Element; export const CatalogIndexPage: (props: DefaultCatalogPageProps) => JSX.Element; // @public @deprecated (undocumented) -export function CatalogKindHeader(props: CatalogKindHeaderProps): JSX.Element; +export function CatalogKindHeader( + props: CatalogKindHeaderProps, +): React_2.JSX.Element; // @public export interface CatalogKindHeaderProps { @@ -137,7 +140,7 @@ export interface CatalogSearchResultListItemProps { // @public (undocumented) export const CatalogTable: { - (props: CatalogTableProps): JSX.Element; + (props: CatalogTableProps): React_2.JSX.Element; columns: Readonly<{ createNameColumn( options?: @@ -303,7 +306,7 @@ export const EntityHasSubcomponentsCard: ( export const EntityHasSystemsCard: (props: HasSystemsCardProps) => JSX.Element; // @public (undocumented) -export const EntityLabelsCard: (props: EntityLabelsCardProps) => JSX.Element; +export const EntityLabelsCard: (props: EntityLabelsCardProps) => JSX_2.Element; // @public (undocumented) export interface EntityLabelsCardProps { @@ -315,7 +318,7 @@ export interface EntityLabelsCardProps { // @public export const EntityLayout: { - (props: EntityLayoutProps): JSX.Element; + (props: EntityLayoutProps): React_2.JSX.Element; Route: (props: EntityLayoutRouteProps) => null; }; @@ -350,7 +353,7 @@ export type EntityLayoutRouteProps = { }; // @public (undocumented) -export const EntityLinksCard: (props: EntityLinksCardProps) => JSX.Element; +export const EntityLinksCard: (props: EntityLinksCardProps) => JSX_2.Element; // @public (undocumented) export interface EntityLinksCardProps { @@ -366,10 +369,10 @@ export type EntityLinksEmptyStateClassKey = 'code'; // @public @deprecated (undocumented) export const EntityListContainer: (props: { children: ReactNode; -}) => JSX.Element; +}) => JSX_2.Element; // @public -export function EntityOrphanWarning(): JSX.Element; +export function EntityOrphanWarning(): React_2.JSX.Element; // @public (undocumented) export interface EntityPredicates { @@ -380,14 +383,14 @@ export interface EntityPredicates { } // @public -export function EntityProcessingErrorsPanel(): JSX.Element | null; +export function EntityProcessingErrorsPanel(): React_2.JSX.Element | null; // @public -export function EntityRelationWarning(): JSX.Element | null; +export function EntityRelationWarning(): React_2.JSX.Element | null; // @public (undocumented) export const EntitySwitch: { - (props: EntitySwitchProps): JSX.Element; + (props: EntitySwitchProps): React_2.JSX.Element; Case: (_props: EntitySwitchCaseProps) => null; }; @@ -428,7 +431,7 @@ export const FilterContainer: (props: { drawerAnchor?: 'left' | 'top' | 'bottom' | 'right' | undefined; } | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public @deprecated (undocumented) export const FilteredEntityLayout: (props: { @@ -445,6 +448,8 @@ export function hasCatalogProcessingErrors( // @public (undocumented) export interface HasComponentsCardProps { + // (undocumented) + title?: string; // (undocumented) variant?: InfoCardVariants; } @@ -462,6 +467,8 @@ export function hasRelationWarnings( // @public (undocumented) export interface HasResourcesCardProps { + // (undocumented) + title?: string; // (undocumented) variant?: InfoCardVariants; } @@ -471,11 +478,15 @@ export interface HasSubcomponentsCardProps { // (undocumented) tableOptions?: TableOptions; // (undocumented) + title?: string; + // (undocumented) variant?: InfoCardVariants; } // @public (undocumented) export interface HasSystemsCardProps { + // (undocumented) + title?: string; // (undocumented) variant?: InfoCardVariants; } diff --git a/plugins/catalog/catalog-info.yaml b/plugins/catalog/catalog-info.yaml new file mode 100644 index 0000000000..5a20949214 --- /dev/null +++ b/plugins/catalog/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog + title: '@backstage/plugin-catalog' + description: The Backstage plugin for browsing the Backstage catalog +spec: + lifecycle: production + type: backstage-frontend-plugin + owner: catalog-maintainers diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 639951fba2..9c6c3874ca 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "1.12.1", + "version": "1.12.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx index c93922e938..d9998b0ca5 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx @@ -59,6 +59,50 @@ describe('EntitySwitch', () => { expect(screen.queryByText('C')).not.toBeInTheDocument(); }); + it('should render the default case if entity is not found', () => { + const content = ( + + + + + + ); + + render( + + + {content} + + , + ); + + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); + expect(screen.queryByText('C')).toBeInTheDocument(); + }); + + it(`shouldn't render any children if entity is loading and no entity exists in the context`, () => { + const content = ( + + + + + + ); + + render( + + + {content} + + , + ); + + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); + expect(screen.queryByText('C')).not.toBeInTheDocument(); + }); + it('should render the fallback if no cases are matching', () => { const content = ( @@ -170,7 +214,7 @@ describe('EntitySwitch', () => { expect(screen.queryByText('A')).not.toBeInTheDocument(); expect(screen.queryByText('B')).not.toBeInTheDocument(); - expect(screen.queryByText('C')).not.toBeInTheDocument(); + expect(screen.queryByText('C')).toBeInTheDocument(); }); it('should switch child when filters switch', () => { diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index b12495052e..9eab743308 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -64,7 +64,7 @@ export interface EntitySwitchProps { /** @public */ export const EntitySwitch = (props: EntitySwitchProps) => { - const { entity } = useAsyncEntity(); + const { entity, loading } = useAsyncEntity(); const apis = useApiHolder(); const results = useElementFilter( @@ -77,14 +77,21 @@ export const EntitySwitch = (props: EntitySwitchProps) => { }) .getElements() .flatMap((element: ReactElement) => { - // If the entity is missing or there is an error, render nothing - if (!entity) { + if (loading && !entity) { return []; } const { if: condition, children: elementsChildren } = element.props as EntitySwitchCase; + if (!entity) { + return [ + { + if: condition === undefined, + children: elementsChildren, + }, + ]; + } return [ { if: condition?.(entity, { apis }), @@ -92,7 +99,7 @@ export const EntitySwitch = (props: EntitySwitchProps) => { }, ]; }), - [apis, entity], + [apis, entity, loading], ); const hasAsyncCases = results.some( diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx index 9654152a5c..9071dac030 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -27,14 +27,15 @@ import { /** @public */ export interface HasComponentsCardProps { variant?: InfoCardVariants; + title?: string; } export function HasComponentsCard(props: HasComponentsCardProps) { - const { variant = 'gridItem' } = props; + const { variant = 'gridItem', title = 'Has components' } = props; return ( ; // @public (undocumented) -export const EntityCircleCIContent: () => JSX.Element; +export const EntityCircleCIContent: () => JSX_2.Element; export { GitType }; @@ -79,5 +81,5 @@ export { isCircleCIAvailable }; export { isCircleCIAvailable as isPluginApplicableToEntity }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 5576faf2ce..e9715bf735 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-circleci", "description": "A Backstage plugin that integrates towards Circle CI", - "version": "0.3.21", + "version": "0.3.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -41,6 +41,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "circleci-api": "^4.0.0", "humanize-duration": "^3.27.0", "lodash": "^4.17.21", @@ -63,7 +64,6 @@ "@testing-library/user-event": "^14.0.0", "@types/humanize-duration": "^3.25.1", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index 99a9a0101f..a9a7c4cec2 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-cloudbuild +## 0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.3.21 ### Patch Changes diff --git a/plugins/cloudbuild/api-report.md b/plugins/cloudbuild/api-report.md index deacf96acd..dc2aacbc18 100644 --- a/plugins/cloudbuild/api-report.md +++ b/plugins/cloudbuild/api-report.md @@ -8,7 +8,9 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -109,17 +111,17 @@ export { cloudbuildPlugin }; export { cloudbuildPlugin as plugin }; // @public (undocumented) -export const EntityCloudbuildContent: () => JSX.Element; +export const EntityCloudbuildContent: () => JSX_2.Element; // @public (undocumented) export const EntityLatestCloudbuildRunCard: (props: { branch: string; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityLatestCloudbuildsForBranchCard: (props: { branch: string; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export interface FETCHSOURCE { @@ -135,12 +137,14 @@ export { isCloudbuildAvailable }; export { isCloudbuildAvailable as isPluginApplicableToEntity }; // @public (undocumented) -export const LatestWorkflowRunCard: (props: { branch: string }) => JSX.Element; +export const LatestWorkflowRunCard: (props: { + branch: string; +}) => React_2.JSX.Element; // @public (undocumented) export const LatestWorkflowsForBranchCard: (props: { branch: string; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export interface Options { @@ -181,7 +185,7 @@ export interface Results { } // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // @public (undocumented) export interface Source { diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index cad764c8b9..ab582d2ae6 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cloudbuild", "description": "A Backstage plugin that integrates towards Google Cloud Build", - "version": "0.3.21", + "version": "0.3.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -41,6 +41,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "qs": "^6.9.4", "react-use": "^17.2.4" @@ -60,7 +61,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md index d1c6181c11..da58cf72f6 100644 --- a/plugins/code-climate/CHANGELOG.md +++ b/plugins/code-climate/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-code-climate +## 0.1.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.21 ### Patch Changes diff --git a/plugins/code-climate/api-report.md b/plugins/code-climate/api-report.md index f4a2d3a26d..32359f663f 100644 --- a/plugins/code-climate/api-report.md +++ b/plugins/code-climate/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -46,7 +47,7 @@ export const codeClimatePlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityCodeClimateCard: () => JSX.Element; +export const EntityCodeClimateCard: () => JSX_2.Element; // @public (undocumented) export class MockCodeClimateApi implements CodeClimateApi { diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json index 1bb57e7f09..99f79667de 100644 --- a/plugins/code-climate/package.json +++ b/plugins/code-climate/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-climate", - "version": "0.1.21", + "version": "0.1.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,6 +36,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "humanize-duration": "^3.27.1", "luxon": "^3.0.0", "react-use": "^17.2.4" @@ -56,7 +57,6 @@ "@types/humanize-duration": "^3.27.1", "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 920cc10980..e429b24b90 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-code-coverage-backend +## 0.2.16-next.0 + +### Patch Changes + +- 3f1367b956c6: Added support for LCOV coverage reports +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + ## 0.2.14 ### Patch Changes diff --git a/plugins/code-coverage-backend/README.md b/plugins/code-coverage-backend/README.md index 802e538125..a60b95ff25 100644 --- a/plugins/code-coverage-backend/README.md +++ b/plugins/code-coverage-backend/README.md @@ -125,6 +125,24 @@ Example: } ``` +### Adding a LCOV report + +POST a LCOV INFO file to `/report` + +Example: + +```json +// curl -X POST -H "Content-Type:text/plain" -d @coverage.info "localhost:7007/api/code-coverage/report?entity=component:default/entity-name&coverageType=lcov" +{ + "links": [ + { + "href": "http://localhost:7007/api/code-coverage/report?entity=component:default/entity-name", + "rel": "coverage" + } + ] +} +``` + ### Reading json coverage GET `/report` diff --git a/plugins/code-coverage-backend/migrations/20210302_init.js b/plugins/code-coverage-backend/migrations/20210302_init.js index a794b6a60c..35eb91541b 100644 --- a/plugins/code-coverage-backend/migrations/20210302_init.js +++ b/plugins/code-coverage-backend/migrations/20210302_init.js @@ -34,7 +34,7 @@ exports.up = async function up(knex) { .comment('An insert counter to ensure ordering'); table.uuid('id').notNullable().comment('The ID of the code coverage'); table - .text('entity') + .string('entity') .notNullable() .comment('The entity ref that this code coverage applies to'); table diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index fe92b964cd..8022424f5e 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.2.14", + "version": "0.2.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts b/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts index 44b1411cc4..23c7c6cba0 100644 --- a/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts +++ b/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts @@ -224,11 +224,11 @@ describe('CodeCoverageUtils', () => { err = error; } expect(err?.message).toEqual( - 'Content-Type header "application/json" not supported, expected "text/xml" possibly followed by a charset', + 'Content-Type header "application/json" not supported, expected "text/xml" or "text/plain" possibly followed by a charset', ); }); - it('parses the body', () => { + it('parses the xml body', () => { const data: Readable = utils.validateRequestBody({ headers: { 'content-type': 'text/xml', @@ -240,6 +240,19 @@ describe('CodeCoverageUtils', () => { expect(data.read()).toContain(' { + const data: Readable = utils.validateRequestBody({ + headers: { + 'content-type': 'text/plain', + }, + body: Readable.from( + 'TN:\nSF:/src/index.js\nFNF:0\nFNH:0\nLF:1\nLH:1\nBRF:0\nBRH:0\nend_of_record', + ), + } as Request); + + expect(data.read()).toMatch(/^TN:\nSF:\/src\/index.js/); + }); }); describe('processCoveragePayload', () => { diff --git a/plugins/code-coverage-backend/src/service/CoverageUtils.ts b/plugins/code-coverage-backend/src/service/CoverageUtils.ts index a4d3405868..bfe79e869f 100644 --- a/plugins/code-coverage-backend/src/service/CoverageUtils.ts +++ b/plugins/code-coverage-backend/src/service/CoverageUtils.ts @@ -163,9 +163,10 @@ export class CoverageUtils { const contentType = req.headers['content-type']; if (!contentType) { throw new InputError('Content-Type header missing'); - } else if (!contentType.match(/^text\/xml($|;)/)) { + // text/xml or text/plain is allowed + } else if (!contentType.match(/^text\/xml|plain($|;)/)) { throw new InputError( - `Content-Type header "${contentType}" not supported, expected "text/xml" possibly followed by a charset`, + `Content-Type header "${contentType}" not supported, expected "text/xml" or "text/plain" possibly followed by a charset`, ); } const body = req.body; diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/lcov-jsoncoverage-files-1.json b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-jsoncoverage-files-1.json new file mode 100644 index 0000000000..1d0ed702df --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-jsoncoverage-files-1.json @@ -0,0 +1,103 @@ +[ + { + "filename": "src/index.js", + "lineHits": { + "1": 1, + "2": 1, + "3": 1, + "4": 5, + "5": 5, + "6": 1, + "7": 5, + "8": 0, + "9": 5, + "10": 3, + "11": 5, + "12": 1, + "13": 5, + "14": 5 + }, + "branchHits": { + "3": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "5": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "7": { + "covered": 0, + "missed": 1, + "available": 1 + }, + "9": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "11": { + "covered": 1, + "missed": 0, + "available": 1 + } + } + }, + { + "filename": "src/math.js", + "lineHits": { + "1": 1, + "2": 1, + "3": 1, + "4": 1, + "5": 1, + "6": 0, + "7": 0, + "8": 1, + "9": 1, + "10": 3, + "11": 0, + "12": 3, + "13": 1, + "14": 3, + "15": 1, + "16": 1, + "17": 1, + "18": 1 + }, + "branchHits": { + "2": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "9": { + "covered": 1, + "missed": 0, + "available": 1 + }, + "10": { + "covered": 0, + "missed": 1, + "available": 1 + }, + "12": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "14": { + "covered": 2, + "missed": 0, + "available": 2 + }, + "17": { + "covered": 1, + "missed": 0, + "available": 1 + } + } + } +] diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/lcov-jsoncoverage-files-2.json b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-jsoncoverage-files-2.json new file mode 100644 index 0000000000..7de4e7dcf7 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-jsoncoverage-files-2.json @@ -0,0 +1,47 @@ +[ + { + "filename": "src/Calc/Main.cs", + "lineHits": { + "5": 4, + "6": 4, + "7": 1, + "8": 0, + "9": 2, + "10": 1, + "11": 4 + }, + "branchHits": { + "5": { + "covered": 5, + "missed": 1, + "available": 6 + } + } + }, + { + "filename": "src/Calc/Math.cs", + "lineHits": { + "5": 1, + "6": 0, + "7": 2, + "8": 2, + "9": 0, + "11": 3, + "12": 1, + "14": 1, + "15": 1 + }, + "branchHits": { + "8": { + "covered": 1, + "missed": 1, + "available": 2 + }, + "11": { + "covered": 2, + "missed": 0, + "available": 2 + } + } + } +] diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/lcov-sourcefiles-1.txt b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-sourcefiles-1.txt new file mode 100644 index 0000000000..4f4bf2c118 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-sourcefiles-1.txt @@ -0,0 +1,2 @@ +src/index.js +src/math.js diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/lcov-sourcefiles-2.txt b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-sourcefiles-2.txt new file mode 100644 index 0000000000..3aa3c36039 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-sourcefiles-2.txt @@ -0,0 +1,2 @@ +src/Calc/Main.cs +src/Calc/Math.cs diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/lcov-testdata-1.info b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-testdata-1.info new file mode 100644 index 0000000000..0bf69f6bc7 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-testdata-1.info @@ -0,0 +1,71 @@ +TN: +SF:src/index.js +FN:3,__vite_ssr_exports__.default +FNF:1 +FNH:1 +FNDA:5,__vite_ssr_exports__.default +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,5 +DA:5,5 +DA:6,1 +DA:7,5 +DA:8,0 +DA:9,5 +DA:10,3 +DA:11,5 +DA:12,1 +DA:13,5 +DA:14,5 +LF:14 +LH:13 +BRDA:3,0,0,5 +BRDA:5,1,0,1 +BRDA:7,2,0,0 +BRDA:9,3,0,3 +BRDA:11,4,0,1 +BRF:5 +BRH:4 +end_of_record +TN: +SF:src/math.js +FN:2,addition +FN:5,subtraction +FN:9,division +FNF:3 +FNH:2 +FNDA:1,addition +FNDA:0,subtraction +FNDA:3,division +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,0 +DA:7,0 +DA:8,1 +DA:9,1 +DA:10,3 +DA:11,0 +DA:12,3 +DA:13,1 +DA:14,3 +DA:15,1 +DA:16,1 +DA:17,1 +DA:18,1 +LF:18 +LH:15 +BRDA:2,0,0,1 +BRDA:9,1,0,3 +BRDA:10,2,0,0 +BRDA:12,3,0,2 +BRDA:12,4,0,1 +BRDA:14,5,0,2 +BRDA:14,6,0,1 +BRDA:17,7,0,1 +BRF:8 +BRH:7 +end_of_record diff --git a/plugins/code-coverage-backend/src/service/__fixtures__/lcov-testdata-2.info b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-testdata-2.info new file mode 100644 index 0000000000..688bffb521 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/__fixtures__/lcov-testdata-2.info @@ -0,0 +1,50 @@ +SF:/Some/Path/src/Calc/Main.cs +FN:4,System.Int32 Calc.Main::Calc(System.Int32,System.Int32,System.String) +FNDA:4,System.Int32 Calc.Main::Calc(System.Int32,System.Int32,System.String) +DA:5,4 +DA:6,4 +DA:7,1 +DA:8,0 +DA:9,2 +DA:10,1 +DA:11,4 +BRDA:5,15,0,3 +BRDA:5,28,0,3 +BRDA:5,15,1,1 +BRDA:5,28,1,0 +BRDA:5,41,1,2 +BRDA:5,41,0,1 +LF:7 +LH:6 +BRF:6 +BRH:5 +FNF:1 +FNH:1 +end_of_record +SF:/Some/Path/src/Calc/Math.cs +FN:4,System.Int32 Calc.Math::Addition(System.Int32,System.Int32) +FNDA:1,System.Int32 Calc.Math::Addition(System.Int32,System.Int32) +DA:5,1 +FN:5,System.Int32 Calc.Math::Subtraction(System.Int32,System.Int32) +FNDA:0,System.Int32 Calc.Math::Subtraction(System.Int32,System.Int32) +DA:6,0 +FN:6,System.Int32 Calc.Math::Division(System.Int32,System.Int32) +FNDA:2,System.Int32 Calc.Math::Division(System.Int32,System.Int32) +DA:7,2 +DA:8,2 +DA:9,0 +DA:11,3 +DA:12,1 +DA:14,1 +DA:15,1 +BRDA:8,7,0,0 +BRDA:8,7,1,2 +BRDA:11,22,0,1 +BRDA:11,22,1,1 +LF:9 +LH:7 +BRF:4 +BRH:3 +FNF:3 +FNH:2 +end_of_record diff --git a/plugins/code-coverage-backend/src/service/converter/index.ts b/plugins/code-coverage-backend/src/service/converter/index.ts index a85f320486..7e6d95ead5 100644 --- a/plugins/code-coverage-backend/src/service/converter/index.ts +++ b/plugins/code-coverage-backend/src/service/converter/index.ts @@ -17,3 +17,4 @@ export * from './Converter'; export { Cobertura } from './cobertura'; export { Jacoco } from './jacoco'; +export { Lcov } from './lcov'; diff --git a/plugins/code-coverage-backend/src/service/converter/lcov.test.ts b/plugins/code-coverage-backend/src/service/converter/lcov.test.ts new file mode 100644 index 0000000000..e31261ba77 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/converter/lcov.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import fs from 'fs'; +import path from 'path'; +import { Lcov } from './lcov'; +import { getVoidLogger } from '@backstage/backend-common'; + +describe('convert lcov', () => { + const converter = new Lcov(getVoidLogger()); + [1, 2].forEach(idx => { + const lcov = fs + .readFileSync( + path.resolve(`${__dirname}/../__fixtures__/lcov-testdata-${idx}.info`), + ) + .toString(); + const expected = JSON.parse( + fs + .readFileSync( + path.resolve( + `${__dirname}/../__fixtures__/lcov-jsoncoverage-files-${idx}.json`, + ), + ) + .toString(), + ); + const scmFiles = fs + .readFileSync( + path.resolve( + `${__dirname}/../__fixtures__/lcov-sourcefiles-${idx}.txt`, + ), + ) + .toString() + .split('\n'); + + it(`should convert the lcov report with index ${idx} to json`, async () => { + const files = converter.convert(lcov, scmFiles); + + expect(files).toEqual(expected); + }); + }); +}); diff --git a/plugins/code-coverage-backend/src/service/converter/lcov.ts b/plugins/code-coverage-backend/src/service/converter/lcov.ts new file mode 100644 index 0000000000..beaa574075 --- /dev/null +++ b/plugins/code-coverage-backend/src/service/converter/lcov.ts @@ -0,0 +1,126 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { FileEntry } from '../types'; +import { Logger } from 'winston'; +import { Converter } from './Converter'; + +export class Lcov implements Converter { + constructor(readonly logger: Logger) { + this.logger = logger; + } + + /** + * convert lcov into shared json coverage format + * + * @param raw - raw lcov report + * @param scmFiles - list of files that are committed to SCM + */ + convert(raw: string, scmFiles: string[]): FileEntry[] { + const lines = raw.split(/\r?\n/); + const jscov: Array = []; + let currentFile: FileEntry | null = null; + + lines.forEach(line => { + const [section, value] = line.split(':'); + + switch (section) { + // If the line starts with SF, it's a new file + case 'SF': + currentFile = this.processNewFile(value, scmFiles); + break; + // If the line starts with DA, it's a line hit + case 'DA': + this.processLineHit(currentFile, value); + break; + // If the line starts with BRDA, it's a branch line + case 'BRDA': + this.processBranchHit(currentFile, value); + break; + // If the line starts with end_of_record, it's the end of current file + case 'end_of_record': + if (currentFile) { + jscov.push(currentFile); + currentFile = null; + } + break; + default: + break; + } + }); + + return jscov; + } + + /** + * Parses a new file entry + * + * @param file - file name from coverage report + * @param scmFiles - list of files that are committed to SCM + */ + private processNewFile(file: string, scmFiles: string[]): FileEntry | null { + const filename = scmFiles.map(f => f.trimEnd()).find(f => file.endsWith(f)); + + this.logger.debug(`matched ${file} to ${filename}`); + + if (filename) { + return { + filename, + lineHits: {}, + branchHits: {}, + }; + } + + return null; + } + + /** + * Parses line coverage information + * + * @param currentFile - current file entry + * @param value - line coverage information + */ + private processLineHit(currentFile: FileEntry | null, value: string): void { + if (!currentFile) return; + + const [lineNumber, hits] = value.split(','); + currentFile.lineHits[Number(lineNumber)] = Number(hits); + } + + /** + * Parses branch coverage information + * + * @param currentFile - current file entry + * @param value - branch coverage information + */ + private processBranchHit(currentFile: FileEntry | null, value: string): void { + if (!currentFile) return; + + const [lineNumber, , , hits] = value.split(','); + const lineNumberNum = Number(lineNumber); + const isHit = Number(hits) > 0; + const branch = currentFile.branchHits[lineNumberNum] || { + covered: 0, + available: 0, + missed: 0, + }; + + branch.available++; + branch.covered += isHit ? 1 : 0; + branch.missed += isHit ? 0 : 1; + + currentFile.branchHits[lineNumberNum] = branch; + } +} diff --git a/plugins/code-coverage-backend/src/service/router.ts b/plugins/code-coverage-backend/src/service/router.ts index 4d4fb779b5..c4f98bd2fb 100644 --- a/plugins/code-coverage-backend/src/service/router.ts +++ b/plugins/code-coverage-backend/src/service/router.ts @@ -30,9 +30,7 @@ import { Config } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { CodeCoverageDatabase } from './CodeCoverageDatabase'; import { aggregateCoverage, CoverageUtils } from './CoverageUtils'; -import { Cobertura } from './converter/cobertura'; -import { Jacoco } from './converter/jacoco'; -import { Converter } from './converter'; +import { Converter, Jacoco, Cobertura, Lcov } from './converter'; import { getEntitySourceLocation } from '@backstage/catalog-model'; /** @@ -182,6 +180,8 @@ export const makeRouter = async ( converter = new Jacoco(logger); } else if (coverageType === 'cobertura') { converter = new Cobertura(logger); + } else if (coverageType === 'lcov') { + converter = new Lcov(logger); } else { throw new InputError(`Unsupported coverage type '${coverageType}`); } diff --git a/plugins/code-coverage-backend/src/service/standaloneServer.ts b/plugins/code-coverage-backend/src/service/standaloneServer.ts index 41493660fb..23eaf981c1 100644 --- a/plugins/code-coverage-backend/src/service/standaloneServer.ts +++ b/plugins/code-coverage-backend/src/service/standaloneServer.ts @@ -16,8 +16,8 @@ import { createServiceBuilder, + HostDiscovery, loadBackendConfig, - SingleHostDiscovery, UrlReaders, useHotMemoize, } from '@backstage/backend-common'; @@ -71,7 +71,7 @@ export async function startStandaloneServer( const router = await createRouter({ database: { getClient: async () => db }, config, - discovery: SingleHostDiscovery.fromConfig(config), + discovery: HostDiscovery.fromConfig(config), urlReader: UrlReaders.default({ logger, config }), logger, catalogApi, diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index 3a3ea6f016..a0e1c9d90c 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-code-coverage +## 0.2.16-next.0 + +### Patch Changes + +- 1d8f4f0a7486: Use fetchApi to ensure authorization is used when fetching code-coverage data +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.2.14 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 3bb5f987a1..647f36cc89 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.2.14", + "version": "0.2.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/code-coverage/src/api.ts b/plugins/code-coverage/src/api.ts index 081f1c7999..942e5a8a47 100644 --- a/plugins/code-coverage/src/api.ts +++ b/plugins/code-coverage/src/api.ts @@ -20,10 +20,13 @@ import { } from '@backstage/catalog-model'; import { ResponseError } from '@backstage/errors'; import { JsonCodeCoverage, JsonCoverageHistory } from './types'; -import { createApiRef, DiscoveryApi } from '@backstage/core-plugin-api'; +import { + createApiRef, + DiscoveryApi, + FetchApi, +} from '@backstage/core-plugin-api'; export type CodeCoverageApi = { - discovery: DiscoveryApi; getCoverageForEntity: ( entity: CompoundEntityRef, ) => Promise; @@ -42,18 +45,22 @@ export const codeCoverageApiRef = createApiRef({ }); export class CodeCoverageRestApi implements CodeCoverageApi { - url: string = ''; + private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; - constructor(public discovery: DiscoveryApi) {} + public constructor(options: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + }) { + this.discoveryApi = options.discoveryApi; + this.fetchApi = options.fetchApi; + } private async fetch( path: string, - init?: RequestInit, ): Promise { - if (!this.url) { - this.url = await this.discovery.getBaseUrl('code-coverage'); - } - const resp = await fetch(`${this.url}${path}`, init); + const url = await this.discoveryApi.getBaseUrl('code-coverage'); + const resp = await this.fetchApi.fetch(`${url}${path}`); if (!resp.ok) { throw await ResponseError.fromResponse(resp); } diff --git a/plugins/code-coverage/src/plugin.ts b/plugins/code-coverage/src/plugin.ts index f137e9d488..391595f142 100644 --- a/plugins/code-coverage/src/plugin.ts +++ b/plugins/code-coverage/src/plugin.ts @@ -21,6 +21,7 @@ import { createPlugin, createRoutableExtension, discoveryApiRef, + fetchApiRef, } from '@backstage/core-plugin-api'; /** @@ -34,8 +35,9 @@ export const codeCoveragePlugin = createPlugin({ apis: [ createApiFactory({ api: codeCoverageApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new CodeCoverageRestApi(discoveryApi), + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new CodeCoverageRestApi({ discoveryApi, fetchApi }), }), ], }); diff --git a/plugins/codescene/CHANGELOG.md b/plugins/codescene/CHANGELOG.md index b81de8f1c6..e2ffc5152c 100644 --- a/plugins/codescene/CHANGELOG.md +++ b/plugins/codescene/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-codescene +## 0.1.17-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + ## 0.1.16 ### Patch Changes diff --git a/plugins/codescene/api-report.md b/plugins/codescene/api-report.md index 75dfddd09e..662968d3c9 100644 --- a/plugins/codescene/api-report.md +++ b/plugins/codescene/api-report.md @@ -7,13 +7,14 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const CodeSceneIcon: IconComponent; // @public (undocumented) -export const CodeScenePage: () => JSX.Element; +export const CodeScenePage: () => JSX_2.Element; // @public (undocumented) export const codescenePlugin: BackstagePlugin< @@ -28,7 +29,7 @@ export const codescenePlugin: BackstagePlugin< >; // @public (undocumented) -export const CodeSceneProjectDetailsPage: () => JSX.Element; +export const CodeSceneProjectDetailsPage: () => JSX_2.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json index 4c5be3949d..2e93b7b663 100644 --- a/plugins/codescene/package.json +++ b/plugins/codescene/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-codescene", - "version": "0.1.16", + "version": "0.1.17-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,6 +36,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "rc-progress": "3.4.2", "react-use": "^17.2.4" }, @@ -54,7 +55,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 6333a686f5..b72750da90 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-config-schema +## 0.1.45-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + ## 0.1.44 ### Patch Changes diff --git a/plugins/config-schema/api-report.md b/plugins/config-schema/api-report.md index 7d110c34d4..95d352d1b7 100644 --- a/plugins/config-schema/api-report.md +++ b/plugins/config-schema/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { Observable } from '@backstage/types'; import { RouteRef } from '@backstage/core-plugin-api'; import { Schema } from 'jsonschema'; @@ -21,7 +22,7 @@ export interface ConfigSchemaApi { export const configSchemaApiRef: ApiRef; // @public (undocumented) -export const ConfigSchemaPage: () => JSX.Element; +export const ConfigSchemaPage: () => JSX_2.Element; // @public (undocumented) export const configSchemaPlugin: BackstagePlugin< diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index c3ffc236e6..f254172e5b 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-config-schema", "description": "A Backstage plugin that lets you browse the configuration schema of your app", - "version": "0.1.44", + "version": "0.1.45-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "jsonschema": "^1.2.6", "react-use": "^17.2.4", "zen-observable": "^0.10.0" @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 027d357687..f63979fad7 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-cost-insights +## 0.12.12-next.0 + +### Patch Changes + +- a54cd411639e: Updated dependency `@types/pluralize` to `^0.0.30`. +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-cost-insights-common@0.1.1 + ## 0.12.10 ### Patch Changes diff --git a/plugins/cost-insights/api-report.md b/plugins/cost-insights/api-report.md index 00cbc4c3af..b37804c93d 100644 --- a/plugins/cost-insights/api-report.md +++ b/plugins/cost-insights/api-report.md @@ -16,6 +16,7 @@ import { Dispatch } from 'react'; import { Entity as Entity_2 } from '@backstage/plugin-cost-insights-common'; import { ForwardRefExoticComponent } from 'react'; import { Group as Group_2 } from '@backstage/plugin-cost-insights-common'; +import { JSX as JSX_2 } from 'react'; import { Maybe as Maybe_2 } from '@backstage/plugin-cost-insights-common'; import { Metric as Metric_2 } from '@backstage/plugin-cost-insights-common'; import { MetricData as MetricData_2 } from '@backstage/plugin-cost-insights-common'; @@ -23,6 +24,7 @@ import { PaletteOptions } from '@material-ui/core/styles/createPalette'; import { Product as Product_2 } from '@backstage/plugin-cost-insights-common'; import { Project as Project_2 } from '@backstage/plugin-cost-insights-common'; import { PropsWithChildren } from 'react'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RefAttributes } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -140,7 +142,7 @@ export enum AlertStatus { } // @public (undocumented) -export const BarChart: (props: BarChartProps) => JSX.Element; +export const BarChart: (props: BarChartProps) => React_2.JSX.Element; // @public @deprecated (undocumented) export interface BarChartData extends BarChartOptions {} @@ -148,7 +150,7 @@ export interface BarChartData extends BarChartOptions {} // @public (undocumented) export const BarChartLegend: ( props: PropsWithChildren, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type BarChartLegendOptions = { @@ -192,12 +194,12 @@ export type BarChartProps = { // @public (undocumented) export const BarChartTooltip: ( props: PropsWithChildren, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export const BarChartTooltipItem: ( props: BarChartTooltipItemProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type BarChartTooltipItemProps = { @@ -247,12 +249,12 @@ export type ConfigContextProps = { export type Cost = common.Cost; // @public (undocumented) -export const CostGrowth: (props: CostGrowthProps) => JSX.Element; +export const CostGrowth: (props: CostGrowthProps) => React_2.JSX.Element; // @public (undocumented) export const CostGrowthIndicator: ( props: CostGrowthIndicatorProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type CostGrowthIndicatorProps = TypographyProps & { @@ -291,10 +293,10 @@ export type CostInsightsApi = { export const costInsightsApiRef: ApiRef; // @public (undocumented) -export const CostInsightsLabelDataflowInstructionsPage: () => JSX.Element; +export const CostInsightsLabelDataflowInstructionsPage: () => JSX_2.Element; // @public (undocumented) -export const CostInsightsPage: () => JSX.Element; +export const CostInsightsPage: () => JSX_2.Element; // @public (undocumented) export type CostInsightsPalette = BackstagePalette & @@ -331,7 +333,7 @@ export { costInsightsPlugin }; export { costInsightsPlugin as plugin }; // @public (undocumented) -export const CostInsightsProjectGrowthInstructionsPage: () => JSX.Element; +export const CostInsightsProjectGrowthInstructionsPage: () => JSX_2.Element; // @public (undocumented) export interface CostInsightsTheme extends BackstageTheme { @@ -418,7 +420,7 @@ export const EngineerThreshold = 0.5; export type Entity = common.Entity; // @public -export const EntityCostInsightsContent: () => JSX.Element; +export const EntityCostInsightsContent: () => JSX_2.Element; // @public (undocumented) export class ExampleCostInsightsClient implements CostInsightsApi { @@ -483,7 +485,7 @@ export enum IconType { // @public (undocumented) export const LegendItem: ( props: PropsWithChildren, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type LegendItemProps = { @@ -507,7 +509,7 @@ export type MetricData = common.MetricData; // @public (undocumented) export const MockConfigProvider: ( props: MockConfigProviderProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type MockConfigProviderProps = PropsWithChildren< @@ -517,7 +519,7 @@ export type MockConfigProviderProps = PropsWithChildren< // @public (undocumented) export const MockCurrencyProvider: ( props: MockCurrencyProviderProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type MockCurrencyProviderProps = PropsWithChildren< @@ -567,7 +569,7 @@ export class ProjectGrowthAlert implements Alert { // (undocumented) data: ProjectGrowthData; // (undocumented) - get element(): JSX.Element; + get element(): React_2.JSX.Element; // (undocumented) get subtitle(): string; // (undocumented) @@ -623,7 +625,7 @@ export class UnlabeledDataflowAlert implements Alert { // (undocumented) data: UnlabeledDataflowData; // (undocumented) - get element(): JSX.Element; + get element(): React_2.JSX.Element; // (undocumented) status?: AlertStatus; // (undocumented) diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 0c490bf645..211b513931 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cost-insights", "description": "A Backstage plugin that helps you keep track of your cloud spend", - "version": "0.12.10", + "version": "0.12.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -70,7 +70,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/pluralize": "^0.0.29", + "@types/pluralize": "^0.0.30", "@types/recharts": "^1.8.14", "@types/regression": "^2.0.0", "@types/yup": "^0.29.13", diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index 2cf41125db..77c7dc4e9e 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-devtools-backend +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-devtools-common@0.1.3 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index ceae4ff428..5a062914de 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.1.3", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md index ec0d9b862b..08794f276c 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-devtools +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-devtools-common@0.1.3 + - @backstage/plugin-permission-react@0.4.15-next.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/devtools/api-report.md b/plugins/devtools/api-report.md index 3e3fb99ff7..f0a9280162 100644 --- a/plugins/devtools/api-report.md +++ b/plugins/devtools/api-report.md @@ -7,15 +7,17 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core'; // @public (undocumented) -export const ConfigContent: () => JSX.Element; +export const ConfigContent: () => React_2.JSX.Element; // @public export const DevToolsLayout: { - ({ children, title, subtitle }: DevToolsLayoutProps): JSX.Element; + ({ children, title, subtitle }: DevToolsLayoutProps): default_2.JSX.Element; Route: (props: SubRoute) => null; }; @@ -27,7 +29,7 @@ export type DevToolsLayoutProps = { }; // @public (undocumented) -export const DevToolsPage: () => JSX.Element; +export const DevToolsPage: () => JSX_2.Element; // @public (undocumented) export const devToolsPlugin: BackstagePlugin< @@ -39,10 +41,10 @@ export const devToolsPlugin: BackstagePlugin< >; // @public (undocumented) -export const ExternalDependenciesContent: () => JSX.Element; +export const ExternalDependenciesContent: () => React_2.JSX.Element; // @public (undocumented) -export const InfoContent: () => JSX.Element; +export const InfoContent: () => React_2.JSX.Element; // @public (undocumented) export type SubRoute = { diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index fbac4d44b6..648fca0143 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools", - "version": "0.1.3", + "version": "0.1.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/dynatrace/CHANGELOG.md b/plugins/dynatrace/CHANGELOG.md index 00e20a1d33..f5b9ddba5b 100644 --- a/plugins/dynatrace/CHANGELOG.md +++ b/plugins/dynatrace/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-dynatrace +## 7.0.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 7.0.1 ### Patch Changes diff --git a/plugins/dynatrace/api-report.md b/plugins/dynatrace/api-report.md index 1dded736ad..0f25fc3108 100644 --- a/plugins/dynatrace/api-report.md +++ b/plugins/dynatrace/api-report.md @@ -7,12 +7,13 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public export const dynatracePlugin: BackstagePlugin<{}, {}, {}>; // @public -export const DynatraceTab: () => JSX.Element; +export const DynatraceTab: () => JSX_2.Element; // @public export const isDynatraceAvailable: (entity: Entity) => boolean; diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json index 69ef692ee2..8459c3d86e 100644 --- a/plugins/dynatrace/package.json +++ b/plugins/dynatrace/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-dynatrace", - "version": "7.0.1", + "version": "7.0.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,6 +35,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -53,7 +54,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "express": "^4.18.1", "msw": "^1.0.0" }, diff --git a/plugins/entity-feedback-backend/CHANGELOG.md b/plugins/entity-feedback-backend/CHANGELOG.md index ce5d04ae38..85a7972e06 100644 --- a/plugins/entity-feedback-backend/CHANGELOG.md +++ b/plugins/entity-feedback-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-entity-feedback-backend +## 0.1.8-next.0 + +### Patch Changes + +- 740155e8e976: Improve backend logging if method calls fail +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-entity-feedback-common@0.1.2 + ## 0.1.6 ### Patch Changes diff --git a/plugins/entity-feedback-backend/README.md b/plugins/entity-feedback-backend/README.md index 99489e28b2..49a8ce20e0 100644 --- a/plugins/entity-feedback-backend/README.md +++ b/plugins/entity-feedback-backend/README.md @@ -4,6 +4,10 @@ Welcome to the entity-feedback backend plugin! ## Installation +Note: this plugin requires authentication and identity configured so Backstage can identify +which user has rated the entity. If you are using the guest identity provider which comes +out of the box, this plugin will not work when you test it. + ### Install the package ```bash diff --git a/plugins/entity-feedback-backend/package.json b/plugins/entity-feedback-backend/package.json index 5cfcebc210..fd634d0e2d 100644 --- a/plugins/entity-feedback-backend/package.json +++ b/plugins/entity-feedback-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-entity-feedback-backend", - "version": "0.1.6", + "version": "0.1.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/entity-feedback-backend/src/service/router.ts b/plugins/entity-feedback-backend/src/service/router.ts index ea759c1639..4953c531b8 100644 --- a/plugins/entity-feedback-backend/src/service/router.ts +++ b/plugins/entity-feedback-backend/src/service/router.ts @@ -134,6 +134,9 @@ export async function createRouter( const user = await identity.getIdentity({ request: req }); const rating = req.body.rating; if (!user || !rating) { + logger.warn( + `Can't save rating because there is not enough info: user=${user}, rating=${rating}`, + ); res.status(400).end(); return; } @@ -188,6 +191,7 @@ export async function createRouter( const { response, comments, consent } = req.body; if (!user) { + logger.warn(`Could not identify user to save responses, user=${user}`); res.status(400).end(); return; } diff --git a/plugins/entity-feedback/CHANGELOG.md b/plugins/entity-feedback/CHANGELOG.md index 44d5d86e70..24bba10a72 100644 --- a/plugins/entity-feedback/CHANGELOG.md +++ b/plugins/entity-feedback/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-entity-feedback +## 0.2.6-next.0 + +### Patch Changes + +- 5e2e06db747a: Improve README to note that Backstage identity is required to be configured +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-entity-feedback-common@0.1.2 + ## 0.2.4 ### Patch Changes diff --git a/plugins/entity-feedback/README.md b/plugins/entity-feedback/README.md index fdfa4761d6..c405bd4083 100644 --- a/plugins/entity-feedback/README.md +++ b/plugins/entity-feedback/README.md @@ -36,7 +36,11 @@ This plugin allows you give and view feedback on entities available in the Backs ## Setup -The following sections will help you get the Entity Feedback plugin setup and running +The following sections will help you get the Entity Feedback plugin setup and running. + +Note: this plugin requires authentication and identity configured so Backstage can identify +which user has rated the entity. If you are using the guest identity provider which comes +out of the box, this plugin will not work when you test it. ### Backend @@ -133,4 +137,4 @@ const groupPage = ( ); ``` -Note: For a full example of this you can look at [this EntityPage](../../packages/app/src/components/catalog/EntityPage.tsx) +Note: For a full example of this you can look at [this EntityPage](../../packages/app/src/components/catalog/EntityPage.tsx). diff --git a/plugins/entity-feedback/api-report.md b/plugins/entity-feedback/api-report.md index 696536aef4..d61394ab9a 100644 --- a/plugins/entity-feedback/api-report.md +++ b/plugins/entity-feedback/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; @@ -14,6 +12,7 @@ import { FeedbackResponse } from '@backstage/plugin-entity-feedback-common'; import { FetchApi } from '@backstage/core-plugin-api'; import { Rating } from '@backstage/plugin-entity-feedback-common'; import { Ratings } from '@backstage/plugin-entity-feedback-common'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -84,18 +83,18 @@ export interface EntityFeedbackResponse { } // @public (undocumented) -export const EntityFeedbackResponseContent: () => JSX.Element; +export const EntityFeedbackResponseContent: () => React_2.JSX.Element; // @public (undocumented) -export const EntityLikeDislikeRatingsCard: () => JSX.Element; +export const EntityLikeDislikeRatingsCard: () => React_2.JSX.Element; // @public (undocumented) -export const EntityStarredRatingsCard: () => JSX.Element; +export const EntityStarredRatingsCard: () => React_2.JSX.Element; // @public (undocumented) export const FeedbackResponseDialog: ( props: FeedbackResponseDialogProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface FeedbackResponseDialogProps { @@ -114,7 +113,7 @@ export interface FeedbackResponseDialogProps { // @public (undocumented) export const FeedbackResponseTable: ( props: FeedbackResponseTableProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface FeedbackResponseTableProps { @@ -127,7 +126,7 @@ export interface FeedbackResponseTableProps { // @public (undocumented) export const LikeDislikeButtons: ( props: LikeDislikeButtonsProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface LikeDislikeButtonsProps { @@ -142,7 +141,7 @@ export interface LikeDislikeButtonsProps { // @public (undocumented) export const LikeDislikeRatingsTable: ( props: LikeDislikeRatingsTableProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface LikeDislikeRatingsTableProps { @@ -157,7 +156,7 @@ export interface LikeDislikeRatingsTableProps { // @public (undocumented) export const StarredRatingButtons: ( props: StarredRatingButtonsProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface StarredRatingButtonsProps { @@ -174,7 +173,7 @@ export interface StarredRatingButtonsProps { // @public (undocumented) export const StarredRatingsTable: ( props: StarredRatingsTableProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface StarredRatingsTableProps { diff --git a/plugins/entity-feedback/package.json b/plugins/entity-feedback/package.json index 2353b4ebed..dfa817ef43 100644 --- a/plugins/entity-feedback/package.json +++ b/plugins/entity-feedback/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-entity-feedback", - "version": "0.2.4", + "version": "0.2.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/entity-validation/CHANGELOG.md b/plugins/entity-validation/CHANGELOG.md index b961397236..c805cc8c2d 100644 --- a/plugins/entity-validation/CHANGELOG.md +++ b/plugins/entity-validation/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-entity-validation +## 0.1.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.6 ### Patch Changes diff --git a/plugins/entity-validation/api-report.md b/plugins/entity-validation/api-report.md index 2fccbf9471..48b309cfaa 100644 --- a/plugins/entity-validation/api-report.md +++ b/plugins/entity-validation/api-report.md @@ -6,13 +6,14 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const EntityValidationPage: (props: { defaultYaml?: string | undefined; defaultLocation?: string | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const entityValidationPlugin: BackstagePlugin< diff --git a/plugins/entity-validation/package.json b/plugins/entity-validation/package.json index 7501c99613..5f7acf3d00 100644 --- a/plugins/entity-validation/package.json +++ b/plugins/entity-validation/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-entity-validation", - "version": "0.1.6", + "version": "0.1.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,6 +43,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@react-hookz/web": "^20.0.0", + "@types/react": "^16.13.1 || ^17.0.0", "@uiw/react-codemirror": "^4.9.3", "lodash": "^4.17.21", "react-use": "^17.2.4", @@ -63,7 +64,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 096469ab72..0fb0e68b7a 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-events-node@0.2.11-next.0 + ## 0.2.3 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 93b9b5c9f6..6f0f970940 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.2.3", + "version": "0.2.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", 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 a76817c3c0..57e1831667 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,11 +14,6 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, - SchedulerService, -} 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'; @@ -34,9 +29,7 @@ describe('eventsModuleAwsSqsConsumingEventPublisher', () => { }, }; - const scheduler = { - scheduleTask: jest.fn(), - } as unknown as SchedulerService; + const scheduler = mockServices.scheduler.mock(); await startTestBackend({ extensionPoints: [[eventsExtensionPoint, extensionPoint]], @@ -68,11 +61,7 @@ describe('eventsModuleAwsSqsConsumingEventPublisher', () => { }, }, }), - createServiceFactory({ - service: coreServices.scheduler, - deps: {}, - factory: () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 51ffe984b5..3bf659bba2 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-azure +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index 7cd26276cf..9a4bb1ab53 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index 6dc7261819..946f1b260c 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 1fafc2d65c..21eab15aa1 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index 26f838d840..6a75c8f9f7 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 4a68964c6e..d6e836573c 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index 1b1674daf5..51a57ebc66 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-github +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-events-node@0.2.11-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index bbfdbd452a..b4fd614b2c 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index eeef12cf74..299b16a149 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-events-node@0.2.11-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index 6b93ea8310..475e88e78e 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index a1f9a6bda2..b646b1b473 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.11-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index b424129ec8..c956304064 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-events-backend-test-utils", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index 4456c0bf99..cbc10a9232 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-events-backend +## 0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-events-node@0.2.11-next.0 + ## 0.2.9 ### Patch Changes diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index f214c729ee..e17f753387 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.2.9", + "version": "0.2.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index b875712724..3befe051a9 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-events-node +## 0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + ## 0.2.9 ### Patch Changes diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 55646356fe..d08c93a12d 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-events-node", "description": "The plugin-events-node module for @backstage/plugin-events-backend", - "version": "0.2.9", + "version": "0.2.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index cfefe9b997..ab7236cb6a 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @internal/plugin-todo-list-backend +## 1.0.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + ## 1.0.16 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index bb0f8af231..f31da2e408 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-backend", - "version": "1.0.16", + "version": "1.0.17-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index f62fcaf029..863223f2e8 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,14 @@ # @internal/plugin-todo-list +## 1.0.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + ## 1.0.16 ### Patch Changes diff --git a/plugins/example-todo-list/api-report.md b/plugins/example-todo-list/api-report.md index 68b3ffd73e..1b7aa49a19 100644 --- a/plugins/example-todo-list/api-report.md +++ b/plugins/example-todo-list/api-report.md @@ -6,10 +6,11 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public -export const TodoListPage: () => JSX.Element; +export const TodoListPage: () => JSX_2.Element; // @public export const todoListPlugin: BackstagePlugin< diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index a219d13df8..8ee7c7e927 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.16", + "version": "1.0.17-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/explore-backend/CHANGELOG.md b/plugins/explore-backend/CHANGELOG.md index 2ac5ef55f1..2484ada01a 100644 --- a/plugins/explore-backend/CHANGELOG.md +++ b/plugins/explore-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-explore-backend +## 0.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-backend-module-explore@0.1.6-next.0 + - @backstage/plugin-search-common@1.2.5 + ## 0.0.10 ### Patch Changes diff --git a/plugins/explore-backend/package.json b/plugins/explore-backend/package.json index a022a13a77..8788eb964c 100644 --- a/plugins/explore-backend/package.json +++ b/plugins/explore-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore-backend", - "version": "0.0.10", + "version": "0.0.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/explore-react/CHANGELOG.md b/plugins/explore-react/CHANGELOG.md index 83bf4cc2a1..789594d742 100644 --- a/plugins/explore-react/CHANGELOG.md +++ b/plugins/explore-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-explore-react +## 0.0.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/plugin-explore-common@0.0.1 + ## 0.0.30 ### Patch Changes diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 55187a6820..1bf38c5c58 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore-react", "description": "A frontend library for Backstage plugins that want to interact with the explore plugin", - "version": "0.0.30", + "version": "0.0.31-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 65eedaedde..4437b812aa 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-explore +## 0.4.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.31-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + ## 0.4.7 ### Patch Changes diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index 70ad479841..193efb044d 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -16,6 +16,8 @@ import { FetchApi } from '@backstage/core-plugin-api'; import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; import { GetExploreToolsResponse } from '@backstage/plugin-explore-common'; import { IndexableDocument } from '@backstage/plugin-search-common'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -36,15 +38,17 @@ export const catalogEntityRouteRef: ExternalRouteRef< export const CatalogKindExploreContent: (props: { title?: string; kind: string; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) -export const DomainCard: (props: { entity: DomainEntity }) => JSX.Element; +export const DomainCard: (props: { + entity: DomainEntity; +}) => React_2.JSX.Element; // @public (undocumented) export const DomainExplorerContent: (props: { title?: string | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public export interface ExploreApi { @@ -67,7 +71,7 @@ export class ExploreClient implements ExploreApi { // @public export const ExploreLayout: { - (props: ExploreLayoutProps): JSX.Element; + (props: ExploreLayoutProps): default_2.JSX.Element; Route: (props: SubRoute) => null; }; @@ -79,7 +83,7 @@ export type ExploreLayoutProps = { }; // @public (undocumented) -export const ExplorePage: () => JSX.Element; +export const ExplorePage: () => JSX_2.Element; // @public (undocumented) const explorePlugin: BackstagePlugin< @@ -107,7 +111,7 @@ export const exploreRouteRef: RouteRef; // @public (undocumented) export const GroupsExplorerContent: (props: { title?: string | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export type SubRoute = { @@ -125,7 +129,7 @@ export type SubRoute = { // @public (undocumented) export const ToolExplorerContent: (props: { title?: string | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const ToolSearchResultListItem: ( diff --git a/plugins/explore/package.json b/plugins/explore/package.json index fa06d3d1f7..c79c3f9390 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore", "description": "A Backstage plugin for building an exploration page of your software ecosystem", - "version": "0.4.7", + "version": "0.4.9-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index fdbdfc845c..71e87cf03e 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-firehydrant +## 0.2.7-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/firehydrant/api-report.md b/plugins/firehydrant/api-report.md index 0ff5bbc62a..76ccd8f019 100644 --- a/plugins/firehydrant/api-report.md +++ b/plugins/firehydrant/api-report.md @@ -7,10 +7,11 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const FirehydrantCard: () => JSX.Element; +export const FirehydrantCard: () => JSX_2.Element; // @public (undocumented) export const firehydrantPlugin: BackstagePlugin< diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 287d40238a..474f78c0a2 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-firehydrant", "description": "A Backstage plugin that integrates towards FireHydrant", - "version": "0.2.5", + "version": "0.2.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -55,7 +56,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index dbf1f4ff76..c5966f4e22 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-fossa +## 0.2.55-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.2.53 ### Patch Changes diff --git a/plugins/fossa/api-report.md b/plugins/fossa/api-report.md index 45f0746ac3..ac76ff6af6 100644 --- a/plugins/fossa/api-report.md +++ b/plugins/fossa/api-report.md @@ -7,15 +7,16 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const EntityFossaCard: (props: { variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const FossaPage: (props: FossaPageProps) => JSX.Element; +export const FossaPage: (props: FossaPageProps) => JSX_2.Element; // @public (undocumented) export type FossaPageProps = { diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 719658d33f..285cc20970 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-fossa", "description": "A Backstage plugin that integrates towards FOSSA", - "version": "0.2.53", + "version": "0.2.55-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,6 +42,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "luxon": "^3.0.0", "p-limit": "^3.0.2", @@ -62,7 +63,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/gcalendar/CHANGELOG.md b/plugins/gcalendar/CHANGELOG.md index 305c72a5ed..cfd8043244 100644 --- a/plugins/gcalendar/CHANGELOG.md +++ b/plugins/gcalendar/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-gcalendar +## 0.3.18-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + ## 0.3.17 ### Patch Changes diff --git a/plugins/gcalendar/api-report.md b/plugins/gcalendar/api-report.md index 8c86e4d2c9..3f1d42d239 100644 --- a/plugins/gcalendar/api-report.md +++ b/plugins/gcalendar/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -55,7 +56,7 @@ export const gcalendarPlugin: BackstagePlugin< >; // @public (undocumented) -export const HomePageCalendar: () => JSX.Element; +export const HomePageCalendar: () => JSX_2.Element; // @public (undocumented) export enum ResponseStatus { diff --git a/plugins/gcalendar/package.json b/plugins/gcalendar/package.json index 9c9a8e6bbe..85be2768e3 100644 --- a/plugins/gcalendar/package.json +++ b/plugins/gcalendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcalendar", - "version": "0.3.17", + "version": "0.3.18-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,6 +37,7 @@ "@material-ui/lab": "4.0.0-alpha.61", "@maxim_mazurok/gapi.client.calendar": "^3.0.20220408", "@tanstack/react-query": "^4.1.3", + "@types/react": "^16.13.1 || ^17.0.0", "classnames": "^2.3.1", "cross-fetch": "^3.1.5", "dompurify": "^2.3.6", @@ -61,7 +62,6 @@ "@testing-library/user-event": "^14.0.0", "@types/dompurify": "^2.3.3", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "@types/sanitize-html": "^2.6.2", "msw": "^1.0.0" }, diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index e80b29da71..5e9089f009 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-gcp-projects +## 0.3.41-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + ## 0.3.40 ### Patch Changes diff --git a/plugins/gcp-projects/api-report.md b/plugins/gcp-projects/api-report.md index 5b7604c935..fd156687c0 100644 --- a/plugins/gcp-projects/api-report.md +++ b/plugins/gcp-projects/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -40,7 +41,7 @@ export class GcpClient implements GcpApi { } // @public (undocumented) -export const GcpProjectsPage: () => JSX.Element; +export const GcpProjectsPage: () => JSX_2.Element; // @public (undocumented) const gcpProjectsPlugin: BackstagePlugin< diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 7915db1dd0..dde867e48f 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gcp-projects", "description": "A Backstage plugin that helps you manage projects in GCP", - "version": "0.3.40", + "version": "0.3.41-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,7 +39,8 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", - "@react-hookz/web": "^20.0.0" + "@react-hookz/web": "^20.0.0", + "@types/react": "^16.13.1 || ^17.0.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", @@ -56,7 +57,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index e88d1c1dfc..d21a66a914 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-git-release-manager +## 0.3.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + ## 0.3.34 ### Patch Changes diff --git a/plugins/git-release-manager/api-report.md b/plugins/git-release-manager/api-report.md index edc2e56ad9..a3215041f0 100644 --- a/plugins/git-release-manager/api-report.md +++ b/plugins/git-release-manager/api-report.md @@ -142,7 +142,7 @@ export interface CreateRcOnSuccessArgs { // Warning: (ae-missing-release-tag) "Differ" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const Differ: ({ current, next, icon }: DifferProps) => JSX.Element; +const Differ: ({ current, next, icon }: DifferProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "DISABLE_CACHE" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -156,7 +156,7 @@ const DISABLE_CACHE: { // Warning: (ae-missing-release-tag) "Divider" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const Divider: () => JSX.Element; +const Divider: () => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "SemverTagParts" needs to be exported by the entry point index.d.ts // @@ -285,7 +285,9 @@ declare namespace helpers { } // @public (undocumented) -const InfoCardPlus: (props: { children?: React_2.ReactNode }) => JSX.Element; +const InfoCardPlus: (props: { + children?: React_2.ReactNode; +}) => React_2.JSX.Element; declare namespace internals { export { components, constants, helpers, testHelpers }; @@ -311,7 +313,7 @@ function isProjectValid(project: any): project is Project; function LinearProgressWithLabel(props: { progress: number; responseSteps: ResponseStep[]; -}): JSX.Element; +}): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "MOCK_RELEASE_BRANCH_NAME_CALVER" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -523,7 +525,7 @@ const mockUsername = 'mock_username'; // Warning: (ae-missing-release-tag) "NoLatestRelease" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const NoLatestRelease: () => JSX.Element; +const NoLatestRelease: () => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "PatchOnSuccessArgs" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -601,7 +603,7 @@ const ResponseStepDialog: ({ progress, responseSteps, title, -}: DialogProps) => JSX.Element; +}: DialogProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ResponseStepListProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ResponseStepList" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -613,7 +615,7 @@ const ResponseStepList: ({ loading, denseList, children, -}: PropsWithChildren) => JSX.Element; +}: PropsWithChildren) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ResponseStepListItemProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ResponseStepListItem" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -622,7 +624,7 @@ const ResponseStepList: ({ const ResponseStepListItem: ({ responseStep, animationDelay, -}: ResponseStepListItemProps) => JSX.Element; +}: ResponseStepListItemProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SEMVER_PARTS" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 1b2adfbe2f..70ab45e546 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.3.34", + "version": "0.3.36-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 3cc8915555..2eb235323f 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-github-actions +## 0.6.4-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.6.2 ### Patch Changes diff --git a/plugins/github-actions/api-report.md b/plugins/github-actions/api-report.md index 0c9d06d90e..e7c6114c93 100644 --- a/plugins/github-actions/api-report.md +++ b/plugins/github-actions/api-report.md @@ -10,6 +10,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RestEndpointMethodTypes } from '@octokit/rest'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmAuthApi } from '@backstage/integration-react'; @@ -27,19 +29,19 @@ export enum BuildStatus { } // @public (undocumented) -export const EntityGithubActionsContent: () => JSX.Element; +export const EntityGithubActionsContent: () => JSX_2.Element; // @public (undocumented) export const EntityLatestGithubActionRunCard: (props: { branch: string; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityLatestGithubActionsForBranchCard: (props: { branch: string; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityRecentGithubActionsRunsCard: (props: { @@ -47,7 +49,7 @@ export const EntityRecentGithubActionsRunsCard: (props: { dense?: boolean | undefined; limit?: number | undefined; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; @@ -208,13 +210,13 @@ export type Jobs = { export const LatestWorkflowRunCard: (props: { branch: string; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const LatestWorkflowsForBranchCard: (props: { branch: string; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const RecentWorkflowRunsCard: (props: { @@ -222,10 +224,10 @@ export const RecentWorkflowRunsCard: (props: { dense?: boolean; limit?: number; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // @public (undocumented) export type Step = { diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index f951503e9e..484281d70f 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.6.2", + "version": "0.6.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -45,6 +45,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@octokit/rest": "^19.0.3", + "@types/react": "^16.13.1 || ^17.0.0", "git-url-parse": "^13.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" @@ -64,7 +65,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index 883cfadc16..adf5cfada5 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-github-deployments +## 0.1.54-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.52 ### Patch Changes diff --git a/plugins/github-deployments/api-report.md b/plugins/github-deployments/api-report.md index 82d2222127..669bcd74ba 100644 --- a/plugins/github-deployments/api-report.md +++ b/plugins/github-deployments/api-report.md @@ -7,6 +7,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { TableColumn } from '@backstage/core-components'; // @public (undocumented) @@ -14,7 +16,7 @@ export const EntityGithubDeploymentsCard: (props: { last?: number | undefined; lastStatuses?: number | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug'; @@ -50,7 +52,7 @@ export const GithubDeploymentsTable: { isLoading: boolean; reload: () => void; columns: TableColumn[]; - }): JSX.Element; + }): React_2.JSX.Element; columns: Readonly<{ createEnvironmentColumn(): TableColumn; createStatusColumn(): TableColumn; diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 05b85aaa60..f4dffa6d22 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-deployments", "description": "A Backstage plugin that integrates towards GitHub Deployments", - "version": "0.1.52", + "version": "0.1.54-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -41,6 +41,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@octokit/graphql": "^5.0.0", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -59,7 +60,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/github-issues/CHANGELOG.md b/plugins/github-issues/CHANGELOG.md index 5ba0696629..103f71470c 100644 --- a/plugins/github-issues/CHANGELOG.md +++ b/plugins/github-issues/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-github-issues +## 0.2.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.2.10 ### Patch Changes diff --git a/plugins/github-issues/api-report.md b/plugins/github-issues/api-report.md index 4e57eeda46..3c7b616f82 100644 --- a/plugins/github-issues/api-report.md +++ b/plugins/github-issues/api-report.md @@ -6,6 +6,7 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -17,7 +18,7 @@ export interface GithubIssuesByRepoOptions { } // @public (undocumented) -export const GithubIssuesCard: (props: GithubIssuesProps) => JSX.Element; +export const GithubIssuesCard: (props: GithubIssuesProps) => JSX_2.Element; // @public (undocumented) export interface GithubIssuesFilters { @@ -44,7 +45,7 @@ export interface GithubIssuesOrdering { } // @public (undocumented) -export const GithubIssuesPage: (props: GithubIssuesProps) => JSX.Element; +export const GithubIssuesPage: (props: GithubIssuesProps) => JSX_2.Element; // @public (undocumented) export const githubIssuesPlugin: BackstagePlugin< diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json index 7cedc25632..dd5bfcbb84 100644 --- a/plugins/github-issues/package.json +++ b/plugins/github-issues/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-issues", - "version": "0.2.10", + "version": "0.2.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.4", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "octokit": "^2.0.4", "react-use": "^17.4.0" @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/github-pull-requests-board/CHANGELOG.md b/plugins/github-pull-requests-board/CHANGELOG.md index 1f630bd0fe..87ffe75c8b 100644 --- a/plugins/github-pull-requests-board/CHANGELOG.md +++ b/plugins/github-pull-requests-board/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-github-pull-requests-board +## 0.1.17-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.15 ### Patch Changes diff --git a/plugins/github-pull-requests-board/api-report.md b/plugins/github-pull-requests-board/api-report.md index 81097118af..24a8f4d926 100644 --- a/plugins/github-pull-requests-board/api-report.md +++ b/plugins/github-pull-requests-board/api-report.md @@ -5,10 +5,12 @@ ```ts /// +import { JSX as JSX_2 } from 'react'; + // @public (undocumented) export const EntityTeamPullRequestsCard: ( props: EntityTeamPullRequestsCardProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export interface EntityTeamPullRequestsCardProps { @@ -19,7 +21,7 @@ export interface EntityTeamPullRequestsCardProps { // @public (undocumented) export const EntityTeamPullRequestsContent: ( props: EntityTeamPullRequestsContentProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export interface EntityTeamPullRequestsContentProps { diff --git a/plugins/github-pull-requests-board/package.json b/plugins/github-pull-requests-board/package.json index ae27d11e50..92de0aae74 100644 --- a/plugins/github-pull-requests-board/package.json +++ b/plugins/github-pull-requests-board/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-pull-requests-board", "description": "A Backstage plugin that allows you to see all open Pull Requests for all the repositories owned by your team", - "version": "0.1.15", + "version": "0.1.17-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -44,6 +44,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@octokit/rest": "^19.0.3", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "p-limit": "^4.0.0", "react-use": "^17.2.4" @@ -62,7 +63,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index 63c6186b72..3eb20ae5a8 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-gitops-profiles +## 0.3.40-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + ## 0.3.39 ### Patch Changes diff --git a/plugins/gitops-profiles/api-report.md b/plugins/gitops-profiles/api-report.md index 9449417bba..3dc3c9add2 100644 --- a/plugins/gitops-profiles/api-report.md +++ b/plugins/gitops-profiles/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -105,13 +106,13 @@ export type GitOpsApi = { export const gitOpsApiRef: ApiRef; // @public (undocumented) -export const GitopsProfilesClusterListPage: () => JSX.Element; +export const GitopsProfilesClusterListPage: () => JSX_2.Element; // @public (undocumented) -export const GitopsProfilesClusterPage: () => JSX.Element; +export const GitopsProfilesClusterPage: () => JSX_2.Element; // @public (undocumented) -export const GitopsProfilesCreatePage: () => JSX.Element; +export const GitopsProfilesCreatePage: () => JSX_2.Element; // @public (undocumented) const gitopsProfilesPlugin: BackstagePlugin< diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 7fed1cf06a..9441e4089b 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gitops-profiles", "description": "A Backstage plugin that helps you manage GitOps profiles", - "version": "0.3.39", + "version": "0.3.40-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -40,6 +40,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md index a38481a3b5..c029f2c235 100644 --- a/plugins/gocd/CHANGELOG.md +++ b/plugins/gocd/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-gocd +## 0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.27 ### Patch Changes diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json index 32e6ce1252..42cc4a1c0d 100644 --- a/plugins/gocd/package.json +++ b/plugins/gocd/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gocd", "description": "A Backstage plugin that integrates towards GoCD", - "version": "0.1.27", + "version": "0.1.29-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index 3c386084c9..4d59e7c499 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-graphiql +## 0.2.54-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + ## 0.2.53 ### Patch Changes diff --git a/plugins/graphiql/api-report.md b/plugins/graphiql/api-report.md index ea765d4d2b..788551476a 100644 --- a/plugins/graphiql/api-report.md +++ b/plugins/graphiql/api-report.md @@ -9,7 +9,9 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ErrorApi } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -36,7 +38,7 @@ export type GithubEndpointConfig = { export const GraphiQLIcon: IconComponent; // @public (undocumented) -export const GraphiQLPage: () => JSX.Element; +export const GraphiQLPage: () => JSX_2.Element; // @public (undocumented) const graphiqlPlugin: BackstagePlugin<{}, {}, {}>; @@ -73,7 +75,7 @@ export class GraphQLEndpoints implements GraphQLBrowseApi { } // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index aacdb84d76..49e3f35e61 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.53", + "version": "0.2.54-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "graphiql": "^1.5.12", "graphql": "^16.0.0", "graphql-ws": "^5.4.1", @@ -59,7 +60,6 @@ "@testing-library/user-event": "^14.0.0", "@types/codemirror": "^5.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/graphql-backend/CHANGELOG.md b/plugins/graphql-backend/CHANGELOG.md index 493aaa81d5..5a1c5b2505 100644 --- a/plugins/graphql-backend/CHANGELOG.md +++ b/plugins/graphql-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-graphql-backend +## 0.1.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-graphql@0.3.22 + ## 0.1.38 ### Patch Changes diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json index a51638c26c..9898f62e81 100644 --- a/plugins/graphql-backend/package.json +++ b/plugins/graphql-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphql-backend", "description": "An experimental Backstage backend plugin for GraphQL", - "version": "0.1.38", + "version": "0.1.40-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/graphql-voyager/CHANGELOG.md b/plugins/graphql-voyager/CHANGELOG.md index 1dc4545aaa..1b9990c67f 100644 --- a/plugins/graphql-voyager/CHANGELOG.md +++ b/plugins/graphql-voyager/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-graphql-voyager +## 0.1.7-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + ## 0.1.6 ### Patch Changes diff --git a/plugins/graphql-voyager/api-report.md b/plugins/graphql-voyager/api-report.md index 9b488f9356..257a8ca7b7 100644 --- a/plugins/graphql-voyager/api-report.md +++ b/plugins/graphql-voyager/api-report.md @@ -7,6 +7,8 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { VoyagerDisplayOptions } from 'graphql-voyager/typings/components/Voyager'; import { WorkerCallback } from 'graphql-voyager/typings/utils/types'; @@ -45,7 +47,7 @@ export class GraphQLVoyagerEndpoints implements GraphQLVoyagerApi { // @public (undocumented) export const GraphqlVoyagerPage: ( props: GraphQLVoyagerPageProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type GraphQLVoyagerPageProps = { @@ -65,7 +67,7 @@ export const graphqlVoyagerPlugin: BackstagePlugin< export const introspectionQuery: string; // @public (undocumented) -export const Router: (props: GraphQLVoyagerPageProps) => JSX.Element; +export const Router: (props: GraphQLVoyagerPageProps) => React_2.JSX.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/graphql-voyager/package.json b/plugins/graphql-voyager/package.json index 814ff95e33..3c7d86c9c6 100644 --- a/plugins/graphql-voyager/package.json +++ b/plugins/graphql-voyager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphql-voyager", "description": "Backstage plugin for GraphQL Voyager", - "version": "0.1.6", + "version": "0.1.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,6 +35,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "graphql-voyager": "^1.0.0-rc.31", "react-use": "^17.2.4" }, @@ -53,7 +54,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/home-react/CHANGELOG.md b/plugins/home-react/CHANGELOG.md index 08aa6f29d9..023e098a38 100644 --- a/plugins/home-react/CHANGELOG.md +++ b/plugins/home-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-home-react +## 0.1.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/home-react/api-report.md b/plugins/home-react/api-report.md index 4806cf7be0..268f598a63 100644 --- a/plugins/home-react/api-report.md +++ b/plugins/home-react/api-report.md @@ -3,9 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { Extension } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RJSFSchema } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; @@ -61,7 +60,7 @@ export function createCardExtension(options: { description?: string; layout?: CardLayout; settings?: CardSettings; -}): Extension<(props: CardExtensionProps) => JSX.Element>; +}): Extension<(props: CardExtensionProps) => React_2.JSX.Element>; // @public (undocumented) export type RendererProps = { @@ -74,5 +73,5 @@ export const SettingsModal: (props: { close: Function; componentName?: string; children: JSX.Element; -}) => JSX.Element; +}) => React_2.JSX.Element; ``` diff --git a/plugins/home-react/catalog-info.yaml b/plugins/home-react/catalog-info.yaml new file mode 100644 index 0000000000..28109b9ddc --- /dev/null +++ b/plugins/home-react/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-home-react + title: '@backstage/plugin-home-react' + description: >- + A Backstage plugin that contains react components helps you build a home + page +spec: + lifecycle: experimental + type: backstage-web-library + owner: discoverability-maintainers diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index 182c5d77f8..486229c84f 100644 --- a/plugins/home-react/package.json +++ b/plugins/home-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-home-react", "description": "A Backstage plugin that contains react components helps you build a home page", - "version": "0.1.2", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,7 +37,8 @@ "@backstage/core-plugin-api": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@rjsf/utils": "5.7.3" + "@rjsf/utils": "5.7.3", + "@types/react": "^16.13.1 || ^17.0.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", @@ -54,7 +55,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "@types/react-grid-layout": "^1.3.2", "cross-fetch": "^3.1.5", "msw": "^1.0.0" diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 52edbdfde0..911ba65068 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-home +## 0.5.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home-react@0.1.3-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.5.5 ### Patch Changes diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md index 32c1c0eb9d..c5e458f78f 100644 --- a/plugins/home/api-report.md +++ b/plugins/home/api-report.md @@ -13,6 +13,7 @@ import { CardSettings as CardSettings_2 } from '@backstage/plugin-home-react'; import { ComponentParts as ComponentParts_2 } from '@backstage/plugin-home-react'; import { ComponentRenderer as ComponentRenderer_2 } from '@backstage/plugin-home-react'; import { createCardExtension as createCardExtension_2 } from '@backstage/plugin-home-react'; +import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; @@ -48,7 +49,7 @@ export const ComponentAccordion: (props: { Actions?: (() => JSX.Element) | undefined; Settings?: (() => JSX.Element) | undefined; ContextProvider?: ((props: any) => JSX.Element) | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public @deprecated (undocumented) export type ComponentParts = ComponentParts_2; @@ -61,7 +62,7 @@ export const ComponentTab: (props: { title: string; Content: () => JSX.Element; ContextProvider?: ((props: any) => JSX.Element) | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const ComponentTabs: (props: { @@ -70,7 +71,7 @@ export const ComponentTabs: (props: { label: string; Component: () => JSX.Element; }[]; -}) => JSX.Element; +}) => JSX_2.Element; // @public @deprecated (undocumented) export const createCardExtension: typeof createCardExtension_2; @@ -78,7 +79,7 @@ export const createCardExtension: typeof createCardExtension_2; // @public export const CustomHomepageGrid: ( props: CustomHomepageGridProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type CustomHomepageGridProps = { @@ -100,36 +101,36 @@ export type CustomHomepageGridProps = { export const HeaderWorldClock: (props: { clockConfigs: ClockConfig[]; customTimeFormat?: Intl.DateTimeFormatOptions | undefined; -}) => JSX.Element | null; +}) => JSX_2.Element | null; // @public export const HomePageCompanyLogo: (props: { logo?: ReactNode; className?: string | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const HomepageCompositionRoot: (props: { title?: string | undefined; children?: ReactNode; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const HomePageRandomJoke: ( props: CardExtensionProps_2<{ defaultCategory?: 'any' | 'programming' | undefined; }>, -) => JSX.Element; +) => JSX_2.Element; // @public export const HomePageStarredEntities: ( props: CardExtensionProps_2, -) => JSX.Element; +) => JSX_2.Element; // @public export const HomePageToolkit: ( props: CardExtensionProps_2, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export const homePlugin: BackstagePlugin< @@ -158,7 +159,7 @@ export const SettingsModal: (props: { close: Function; componentName?: string | undefined; children: JSX.Element; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const TemplateBackstageLogo: (props: { @@ -166,10 +167,10 @@ export const TemplateBackstageLogo: (props: { svg: string; path: string; }; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) -export const TemplateBackstageLogoIcon: () => JSX.Element; +export const TemplateBackstageLogoIcon: () => React_2.JSX.Element; // @public (undocumented) export type Tool = { @@ -186,7 +187,7 @@ export type ToolkitContentProps = { // @public export const WelcomeTitle: ({ language, -}: WelcomeTitleLanguageProps) => JSX.Element; +}: WelcomeTitleLanguageProps) => JSX_2.Element; // @public (undocumented) export type WelcomeTitleLanguageProps = { diff --git a/plugins/home/catalog-info.yaml b/plugins/home/catalog-info.yaml new file mode 100644 index 0000000000..1b23d4b83b --- /dev/null +++ b/plugins/home/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-home + title: '@backstage/plugin-home' + description: A Backstage plugin that helps you build a home page +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: discoverability-maintainers diff --git a/plugins/home/package.json b/plugins/home/package.json index 74a692011e..f732558a3b 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-home", "description": "A Backstage plugin that helps you build a home page", - "version": "0.5.5", + "version": "0.5.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index 3d436ea912..a9b87ae791 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-ilert +## 0.2.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.2.10 ### Patch Changes diff --git a/plugins/ilert/api-report.md b/plugins/ilert/api-report.md index 6cc0c12a8f..b2081e049e 100644 --- a/plugins/ilert/api-report.md +++ b/plugins/ilert/api-report.md @@ -11,6 +11,8 @@ import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IconComponent } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -302,7 +304,7 @@ export type AlertStatus = typeof PENDING | typeof ACCEPTED | typeof RESOLVED; export const DEGRADED = 'DEGRADED'; // @public (undocumented) -export const EntityILertCard: () => JSX.Element; +export const EntityILertCard: () => JSX_2.Element; // @public (undocumented) export interface EscalationPolicy { @@ -442,7 +444,7 @@ export interface ILertApi { export const ilertApiRef: ApiRef; // @public (undocumented) -export const ILertCard: () => JSX.Element; +export const ILertCard: () => React_2.JSX.Element; // @public (undocumented) export class ILertClient implements ILertApi { @@ -530,7 +532,7 @@ export class ILertClient implements ILertApi { export const ILertIcon: IconComponent; // @public (undocumented) -export const ILertPage: () => JSX.Element; +export const ILertPage: () => JSX_2.Element; // @public (undocumented) const ilertPlugin: BackstagePlugin< @@ -649,7 +651,7 @@ export interface Responder { } // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // @public (undocumented) export interface Schedule { diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index c9a5320941..5499f0195a 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-ilert", "description": "A Backstage plugin that integrates towards iLert", - "version": "0.2.10", + "version": "0.2.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -40,6 +40,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/pickers": "^3.3.10", + "@types/react": "^16.13.1 || ^17.0.0", "humanize-duration": "^3.26.0", "luxon": "^3.0.0", "prop-types": "^15.7.2", @@ -60,7 +61,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 3e63cdd4a0..fa3d083760 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-jenkins-backend +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-jenkins-common@0.1.18 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + ## 0.2.3 ### Patch Changes diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 796190fc5e..89e5b184d6 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.2.3", + "version": "0.2.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 92718880b5..4928dcb174 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-jenkins +## 0.8.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-jenkins-common@0.1.18 + ## 0.8.3 ### Patch Changes diff --git a/plugins/jenkins/api-report.md b/plugins/jenkins/api-report.md index a2ae16988b..0782f0abd2 100644 --- a/plugins/jenkins/api-report.md +++ b/plugins/jenkins/api-report.md @@ -12,16 +12,18 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EntityJenkinsContent: () => JSX.Element; +export const EntityJenkinsContent: () => JSX_2.Element; // @public (undocumented) export const EntityLatestJenkinsRunCard: (props: { branch: string; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) const isJenkinsAvailable: (entity: Entity) => boolean; @@ -109,7 +111,7 @@ export { jenkinsPlugin as plugin }; export const LatestRunCard: (props: { branch: string; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "LEGACY_JENKINS_ANNOTATION" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -119,5 +121,5 @@ export const LEGACY_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; // Warning: (ae-missing-release-tag) "Router" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index ddc100d03e..a5b9f2dc50 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.8.3", + "version": "0.8.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,6 +43,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -61,7 +62,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "@types/testing-library__jest-dom": "^5.9.1", "cross-fetch": "^3.1.5", "msw": "^1.0.0" diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index dc3c754fba..5bd8945358 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kafka-backend +## 0.2.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + ## 0.2.41 ### Patch Changes diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index fd2ad7659f..911a03af79 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka-backend", "description": "A Backstage backend plugin that integrates towards Kafka", - "version": "0.2.41", + "version": "0.2.43-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index b728968a93..9e155ce9f8 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-kafka +## 0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.3.21 ### Patch Changes diff --git a/plugins/kafka/api-report.md b/plugins/kafka/api-report.md index 18b51b77fa..990a4da295 100644 --- a/plugins/kafka/api-report.md +++ b/plugins/kafka/api-report.md @@ -7,10 +7,12 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EntityKafkaContent: () => JSX.Element; +export const EntityKafkaContent: () => JSX_2.Element; // @public (undocumented) const isPluginApplicableToEntity: (entity: Entity) => boolean; @@ -33,5 +35,5 @@ export { kafkaPlugin }; export { kafkaPlugin as plugin }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index b2f8b6fe64..1ae4d414a0 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.3.21", + "version": "0.3.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,6 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -57,7 +58,6 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "jest-when": "^3.1.0", "msw": "^1.0.0" }, diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 67a2e800aa..a3a07be104 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-kubernetes-backend +## 0.11.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + ## 0.11.3 ### Patch Changes diff --git a/plugins/kubernetes-backend/catalog-info.yaml b/plugins/kubernetes-backend/catalog-info.yaml new file mode 100644 index 0000000000..cd1026d01d --- /dev/null +++ b/plugins/kubernetes-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-kubernetes-backend + title: '@backstage/plugin-kubernetes-backend' + description: A Backstage backend plugin that integrates towards Kubernetes +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: kubernetes-maintainers diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 33b2b4fe28..314010f574 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.11.3", + "version": "0.11.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index 81ee863cb7..f436024131 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -29,7 +29,7 @@ const mockCatalogApi = { annotations: { 'kubernetes.io/api-server': 'https://apiserver.com', 'kubernetes.io/api-server-certificate-authority': 'caData', - 'kubernetes.io/auth-provider': 'aws', + 'kubernetes.io/auth-provider': 'oidc', 'kubernetes.io/oidc-token-provider': 'google', 'kubernetes.io/skip-metrics-lookup': 'true', 'kubernetes.io/skip-tls-verify': 'true', @@ -40,6 +40,24 @@ const mockCatalogApi = { namespace: 'default', }, }, + { + apiVersion: 'version', + kind: 'User', + metadata: { + annotations: { + 'kubernetes.io/api-server': 'https://apiserver.com', + 'kubernetes.io/api-server-certificate-authority': 'caData', + 'kubernetes.io/auth-provider': 'aws', + 'kubernetes.io/aws-assume-role': 'my-role', + 'kubernetes.io/aws-external-id': 'my-id', + 'kubernetes.io/oidc-token-provider': 'google', + 'kubernetes.io/dashboard-url': 'my-url', + 'kubernetes.io/dashboard-app': 'my-app', + }, + name: 'owned', + namespace: 'default', + }, + }, ], }), } as unknown as CatalogApi; @@ -67,12 +85,12 @@ describe('CatalogClusterLocator', () => { const result = await clusterSupplier.getClusters(); - expect(result).toHaveLength(1); + expect(result).toHaveLength(2); expect(result[0]).toStrictEqual({ name: 'owned', url: 'https://apiserver.com', caData: 'caData', - authProvider: 'aws', + authProvider: 'oidc', oidcTokenProvider: 'google', skipMetricsLookup: true, skipTLSVerify: true, @@ -80,4 +98,25 @@ describe('CatalogClusterLocator', () => { dashboardApp: 'my-app', }); }); + + it('returns the aws cluster details provided by annotations', async () => { + const clusterSupplier = CatalogClusterLocator.fromConfig(mockCatalogApi); + + const result = await clusterSupplier.getClusters(); + + expect(result).toHaveLength(2); + expect(result[1]).toStrictEqual({ + name: 'owned', + url: 'https://apiserver.com', + caData: 'caData', + authProvider: 'aws', + assumeRole: 'my-role', + externalId: 'my-id', + oidcTokenProvider: 'google', + skipMetricsLookup: false, + skipTLSVerify: false, + dashboardUrl: 'my-url', + dashboardApp: 'my-app', + }); + }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index b952aefccf..c48cc99eb6 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; +import { + AWSClusterDetails, + ClusterDetails, + KubernetesClustersSupplier, +} from '../types/types'; import { CATALOG_FILTER_EXISTS, CatalogApi } from '@backstage/catalog-client'; import { ANNOTATION_KUBERNETES_API_SERVER, @@ -25,6 +29,8 @@ import { ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY, ANNOTATION_KUBERNETES_DASHBOARD_URL, ANNOTATION_KUBERNETES_DASHBOARD_APP, + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, } from '@backstage/plugin-kubernetes-common'; export class CatalogClusterLocator implements KubernetesClustersSupplier { @@ -69,21 +75,31 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { skipMetricsLookup: entity.metadata.annotations![ ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP - ]! === 'true' - ? true - : false, + ]! === 'true', skipTLSVerify: entity.metadata.annotations![ ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY - ]! === 'true' - ? true - : false, + ]! === 'true', dashboardUrl: entity.metadata.annotations![ANNOTATION_KUBERNETES_DASHBOARD_URL]!, dashboardApp: entity.metadata.annotations![ANNOTATION_KUBERNETES_DASHBOARD_APP]!, }; + if (clusterDetails.authProvider === 'aws') { + return { + ...clusterDetails, + assumeRole: + entity.metadata.annotations![ + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE + ]!, + externalId: + entity.metadata.annotations![ + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID + ]!, + } as AWSClusterDetails; + } + return clusterDetails; }); } diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index 56faeea7d9..aae72d6f1e 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -31,6 +31,14 @@ export const ANNOTATION_KUBERNETES_API_SERVER_CA = export const ANNOTATION_KUBERNETES_AUTH_PROVIDER = 'kubernetes.io/auth-provider'; +// @public +export const ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE = + 'kubernetes.io/aws-assume-role'; + +// @public +export const ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID = + 'kubernetes.io/aws-external-id'; + // @public export const ANNOTATION_KUBERNETES_DASHBOARD_APP = 'kubernetes.io/dashboard-app'; diff --git a/plugins/kubernetes-common/catalog-info.yaml b/plugins/kubernetes-common/catalog-info.yaml new file mode 100644 index 0000000000..be2e47bbbe --- /dev/null +++ b/plugins/kubernetes-common/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-kubernetes-common + title: '@backstage/plugin-kubernetes-common' + description: >- + Common functionalities for kubernetes, to be shared between kubernetes and + kubernetes-backend plugin +spec: + lifecycle: experimental + type: backstage-common-library + owner: kubernetes-maintainers diff --git a/plugins/kubernetes-common/src/catalog-entity-constants.ts b/plugins/kubernetes-common/src/catalog-entity-constants.ts index a1a1996777..8a826d4316 100644 --- a/plugins/kubernetes-common/src/catalog-entity-constants.ts +++ b/plugins/kubernetes-common/src/catalog-entity-constants.ts @@ -76,3 +76,19 @@ export const ANNOTATION_KUBERNETES_DASHBOARD_URL = */ export const ANNOTATION_KUBERNETES_DASHBOARD_APP = 'kubernetes.io/dashboard-app'; + +/** + * Annotation for specifying the assume role use to authenticate with AWS. + * + * @public + */ +export const ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE = + 'kubernetes.io/aws-assume-role'; + +/** + * Annotation for specifying an external id when communicating with AWS + * + * @public + */ +export const ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID = + 'kubernetes.io/aws-external-id'; diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index fc1a60d305..fe85220072 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-kubernetes +## 0.10.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + ## 0.10.0 ### Minor Changes diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 9d67ecba83..3c91de02ac 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -30,6 +30,7 @@ import { OpenIdConnectApi } from '@backstage/core-plugin-api'; import { Pod } from 'kubernetes-models/v1/Pod'; import { Pod as Pod_2 } from 'kubernetes-models/v1'; import { default as React_2 } from 'react'; +import * as React_3 from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TypeMeta } from '@kubernetes-models/base'; import { V1ConfigMap } from '@kubernetes/client-node'; @@ -52,7 +53,7 @@ import { WorkloadsByEntityRequest } from '@backstage/plugin-kubernetes-common'; export const Cluster: ({ clusterObjects, podsWithErrors, -}: ClusterProps) => JSX.Element; +}: ClusterProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "ClusterContext" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -110,13 +111,13 @@ export interface ContainerScope extends PodScope { // Warning: (ae-missing-release-tag) "CronJobsAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const CronJobsAccordions: ({}: CronJobsAccordionsProps) => JSX.Element; +export const CronJobsAccordions: ({}: CronJobsAccordionsProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "CustomResourcesProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "CustomResources" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const CustomResources: ({}: CustomResourcesProps) => JSX.Element; +export const CustomResources: ({}: CustomResourcesProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "DeploymentResources" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -174,7 +175,9 @@ export type EntityKubernetesContentProps = { }; // @public -export const ErrorList: ({ podAndErrors }: ErrorListProps) => JSX.Element; +export const ErrorList: ({ + podAndErrors, +}: ErrorListProps) => React_2.JSX.Element; // @public export interface ErrorListProps { @@ -195,7 +198,7 @@ export const ErrorPanel: ({ entityName, errorMessage, clustersWithErrors, -}: ErrorPanelProps) => JSX.Element; +}: ErrorPanelProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ErrorReportingProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ErrorReporting" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -203,7 +206,7 @@ export const ErrorPanel: ({ // @public (undocumented) export const ErrorReporting: ({ detectedErrors, -}: ErrorReportingProps) => JSX.Element; +}: ErrorReportingProps) => React_3.JSX.Element; // @public export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; @@ -214,13 +217,13 @@ export const Events: ({ namespace, clusterName, warningEventsOnly, -}: EventsProps) => JSX.Element; +}: EventsProps) => React_2.JSX.Element; // @public export const EventsContent: ({ events, warningEventsOnly, -}: EventsContentProps) => JSX.Element; +}: EventsContentProps) => React_2.JSX.Element; // @public export interface EventsContentProps { @@ -323,13 +326,13 @@ export const HorizontalPodAutoscalerDrawer: (props: { hpa: V1HorizontalPodAutoscaler; expanded?: boolean; children?: React_2.ReactNode; -}) => JSX.Element; +}) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "IngressesAccordionsProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IngressesAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const IngressesAccordions: ({}: IngressesAccordionsProps) => JSX.Element; +export const IngressesAccordions: ({}: IngressesAccordionsProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "isKubernetesAvailable" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -340,7 +343,9 @@ export const isKubernetesAvailable: (entity: Entity) => boolean; // Warning: (ae-missing-release-tag) "JobsAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const JobsAccordions: ({ jobs }: JobsAccordionsProps) => JSX.Element; +export const JobsAccordions: ({ + jobs, +}: JobsAccordionsProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "KubernetesApi" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -464,7 +469,7 @@ export class KubernetesBackendClient implements KubernetesApi { export const KubernetesContent: ({ entity, refreshIntervalMs, -}: KubernetesContentProps) => JSX.Element; +}: KubernetesContentProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "KubernetesDrawerProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "KubernetesDrawer" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -476,7 +481,7 @@ export const KubernetesDrawer: ({ drawerContentsHeader, kubernetesObject, children, -}: KubernetesDrawerProps) => JSX.Element; +}: KubernetesDrawerProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "KubernetesDrawerContentProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "KubernetesDrawerContent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -487,7 +492,7 @@ export const KubernetesDrawerContent: ({ header, kubernetesObject, close, -}: KubernetesDrawerContentProps) => JSX.Element; +}: KubernetesDrawerContentProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "KubernetesObjects" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -586,7 +591,7 @@ export const KubernetesStructuredMetadataTableDrawer: < buttonVariant, expanded, children, -}: KubernetesStructuredMetadataTableDrawerProps) => JSX.Element; +}: KubernetesStructuredMetadataTableDrawerProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ErrorPanelProps_2" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "LinkErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -595,12 +600,12 @@ export const KubernetesStructuredMetadataTableDrawer: < export const LinkErrorPanel: ({ cluster, errorMessage, -}: ErrorPanelProps_2) => JSX.Element; +}: ErrorPanelProps_2) => React_2.JSX.Element; // @public export const PendingPodContent: ({ pod, -}: PendingPodContentProps) => JSX.Element; +}: PendingPodContentProps) => React_2.JSX.Element; // @public export interface PendingPodContentProps { @@ -621,7 +626,10 @@ export interface PodAndErrors { // Warning: (ae-forgotten-export) The symbol "PodDrawerProps" needs to be exported by the entry point index.d.ts // // @public -export const PodDrawer: ({ podAndErrors, open }: PodDrawerProps) => JSX.Element; +export const PodDrawer: ({ + podAndErrors, + open, +}: PodDrawerProps) => React_2.JSX.Element; // @public export const PodLogs: React_2.FC; @@ -629,7 +637,7 @@ export const PodLogs: React_2.FC; // @public export const PodLogsDialog: ({ containerScope, -}: PodLogsDialogProps) => JSX.Element; +}: PodLogsDialogProps) => React_2.JSX.Element; // @public export interface PodLogsDialogProps { @@ -692,7 +700,7 @@ export interface PodScope { export const PodsTable: ({ pods, extraColumns, -}: PodsTablesProps) => JSX.Element; +}: PodsTablesProps) => React_2.JSX.Element; // @public export interface ResourceRef { @@ -715,12 +723,14 @@ export const ResourceUtilization: ({ usage, total, totalFormatted, -}: ResourceUtilizationProps) => JSX.Element; +}: ResourceUtilizationProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "Router" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: (props: { refreshIntervalMs?: number }) => JSX.Element; +export const Router: (props: { + refreshIntervalMs?: number; +}) => React_2.JSX.Element; // @public export class ServerSideKubernetesAuthProvider @@ -738,7 +748,7 @@ export class ServerSideKubernetesAuthProvider // Warning: (ae-missing-release-tag) "ServicesAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ServicesAccordions: ({}: ServicesAccordionsProps) => JSX.Element; +export const ServicesAccordions: ({}: ServicesAccordionsProps) => React_2.JSX.Element; // @public export const useCustomResources: ( diff --git a/plugins/kubernetes/catalog-info.yaml b/plugins/kubernetes/catalog-info.yaml new file mode 100644 index 0000000000..8f96a0c10c --- /dev/null +++ b/plugins/kubernetes/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-kubernetes + title: '@backstage/plugin-kubernetes' + description: A Backstage plugin that integrates towards Kubernetes +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: kubernetes-maintainers diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 76d6452e0c..832920cef3 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.10.0", + "version": "0.10.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/lighthouse-backend/CHANGELOG.md b/plugins/lighthouse-backend/CHANGELOG.md index 75bb0168a8..3d1d40b7fd 100644 --- a/plugins/lighthouse-backend/CHANGELOG.md +++ b/plugins/lighthouse-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-lighthouse-backend +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-lighthouse-common@0.1.2 + ## 0.2.4 ### Patch Changes diff --git a/plugins/lighthouse-backend/package.json b/plugins/lighthouse-backend/package.json index 603b3dbac0..85cee75254 100644 --- a/plugins/lighthouse-backend/package.json +++ b/plugins/lighthouse-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse-backend", "description": "Backend functionalities for lighthouse", - "version": "0.2.4", + "version": "0.2.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 3c638bc89f..8d91120894 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-lighthouse +## 0.4.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-lighthouse-common@0.1.2 + ## 0.4.6 ### Patch Changes diff --git a/plugins/lighthouse/api-report.md b/plugins/lighthouse/api-report.md index 4ab851472a..9794fe1728 100644 --- a/plugins/lighthouse/api-report.md +++ b/plugins/lighthouse/api-report.md @@ -9,20 +9,22 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { LighthouseApi } from '@backstage/plugin-lighthouse-common'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EmbeddedRouter: () => JSX.Element; +export const EmbeddedRouter: () => React_2.JSX.Element; // @public (undocumented) export const EntityLastLighthouseAuditCard: (props: { dense?: boolean | undefined; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const EntityLighthouseContent: () => JSX.Element; +export const EntityLighthouseContent: () => JSX_2.Element; // @public (undocumented) const isLighthouseAvailable: (entity: Entity) => boolean; @@ -33,13 +35,13 @@ export { isLighthouseAvailable as isPluginApplicableToEntity }; export const LastLighthouseAuditCard: (props: { dense?: boolean; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const lighthouseApiRef: ApiRef; // @public (undocumented) -export const LighthousePage: () => JSX.Element; +export const LighthousePage: () => JSX_2.Element; // @public (undocumented) const lighthousePlugin: BackstagePlugin< @@ -54,5 +56,5 @@ export { lighthousePlugin }; export { lighthousePlugin as plugin }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 990e789613..040ad0dfa2 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.4.6", + "version": "0.4.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,6 +43,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -61,7 +62,6 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/linguist-backend/CHANGELOG.md b/plugins/linguist-backend/CHANGELOG.md index 9af630bf64..28bab3eb9e 100644 --- a/plugins/linguist-backend/CHANGELOG.md +++ b/plugins/linguist-backend/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-linguist-backend +## 0.4.2-next.0 + +### Patch Changes + +- b2de501bda31: When creating the router using `createRouterFromConfig` or using the plugin for the new backend system the `linguist.useSourceLocation` configuration is now optional. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-linguist-common@0.1.1 + ## 0.4.0 ### Minor Changes diff --git a/plugins/linguist-backend/migrations/20221115_init.js b/plugins/linguist-backend/migrations/20221115_init.js index 3bcf99e08b..359f7c8359 100644 --- a/plugins/linguist-backend/migrations/20221115_init.js +++ b/plugins/linguist-backend/migrations/20221115_init.js @@ -32,7 +32,7 @@ exports.up = async function up(knex) { .comment('An insert counter to ensure ordering'); table.uuid('id').notNullable().comment('The ID of the Linguist result'); table - .text('entity_ref') + .string('entity_ref') .unique() .notNullable() .comment('The entity ref that this Linguist result applies to'); diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json index 618e548195..14e65238b1 100644 --- a/plugins/linguist-backend/package.json +++ b/plugins/linguist-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-linguist-backend", - "version": "0.4.0", + "version": "0.4.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/linguist-backend/src/service/router.ts b/plugins/linguist-backend/src/service/router.ts index 02ff05f398..d29943c38b 100644 --- a/plugins/linguist-backend/src/service/router.ts +++ b/plugins/linguist-backend/src/service/router.ts @@ -150,9 +150,8 @@ export async function createRouterFromConfig(routerOptions: RouterOptions) { ); } pluginOptions.batchSize = config.getOptionalNumber('linguist.batchSize'); - pluginOptions.useSourceLocation = config.getBoolean( - 'linguist.useSourceLocation', - ); + pluginOptions.useSourceLocation = + config.getOptionalBoolean('linguist.useSourceLocation') ?? false; pluginOptions.age = config.getOptionalConfig('linguist.age') as | HumanDuration | undefined; diff --git a/plugins/linguist/CHANGELOG.md b/plugins/linguist/CHANGELOG.md index 55c9cdcedb..a42c9ef7ad 100644 --- a/plugins/linguist/CHANGELOG.md +++ b/plugins/linguist/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-linguist +## 0.1.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-linguist-common@0.1.1 + ## 0.1.6 ### Patch Changes diff --git a/plugins/linguist/api-report.md b/plugins/linguist/api-report.md index 469b3b93c0..5182ce12f6 100644 --- a/plugins/linguist/api-report.md +++ b/plugins/linguist/api-report.md @@ -7,9 +7,10 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) -export const EntityLinguistCard: () => JSX.Element; +export const EntityLinguistCard: () => JSX_2.Element; // @public (undocumented) export const isLinguistAvailable: (entity: Entity) => boolean; diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json index 45a24fcd24..8f9e902428 100644 --- a/plugins/linguist/package.json +++ b/plugins/linguist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-linguist", - "version": "0.1.6", + "version": "0.1.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,6 +38,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^2.0.2", "react-use": "^17.2.4", "slugify": "^1.6.4" @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/microsoft-calendar/CHANGELOG.md b/plugins/microsoft-calendar/CHANGELOG.md index ae8ec5ea3d..57ef8c7987 100644 --- a/plugins/microsoft-calendar/CHANGELOG.md +++ b/plugins/microsoft-calendar/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-microsoft-calendar +## 0.1.7-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + ## 0.1.6 ### Patch Changes diff --git a/plugins/microsoft-calendar/api-report.md b/plugins/microsoft-calendar/api-report.md index d6839f19ea..fad48e5de0 100644 --- a/plugins/microsoft-calendar/api-report.md +++ b/plugins/microsoft-calendar/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { Calendar } from '@microsoft/microsoft-graph-types'; import { Event as Event_2 } from '@microsoft/microsoft-graph-types'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -33,7 +34,7 @@ export class MicrosoftCalendarApiClient { export const microsoftCalendarApiRef: ApiRef; // @public (undocumented) -export const MicrosoftCalendarCard: () => JSX.Element; +export const MicrosoftCalendarCard: () => JSX_2.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/microsoft-calendar/package.json b/plugins/microsoft-calendar/package.json index 5003f887c1..8ad7036f8c 100644 --- a/plugins/microsoft-calendar/package.json +++ b/plugins/microsoft-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-microsoft-calendar", - "version": "0.1.6", + "version": "0.1.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -55,6 +55,7 @@ "@material-ui/icons": "^4.9.1", "@microsoft/microsoft-graph-types": "^2.25.0", "@tanstack/react-query": "^4.1.3", + "@types/react": "^16.13.1 || ^17.0.0", "classnames": "^2.3.1", "dompurify": "^2.3.6", "lodash": "^4.17.21", @@ -77,7 +78,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md index cc21a7d6b7..042f789e2e 100644 --- a/plugins/newrelic-dashboard/CHANGELOG.md +++ b/plugins/newrelic-dashboard/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-newrelic-dashboard +## 0.2.16-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.2.14 ### Patch Changes diff --git a/plugins/newrelic-dashboard/api-report.md b/plugins/newrelic-dashboard/api-report.md index 740fc46ef7..2e9b11a88e 100644 --- a/plugins/newrelic-dashboard/api-report.md +++ b/plugins/newrelic-dashboard/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -14,13 +15,13 @@ export const DashboardSnapshotComponent: (props: { guid: string; name: string; permalink: string; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const EntityNewRelicDashboardCard: () => JSX.Element; +export const EntityNewRelicDashboardCard: () => JSX_2.Element; // @public (undocumented) -export const EntityNewRelicDashboardContent: () => JSX.Element; +export const EntityNewRelicDashboardContent: () => JSX_2.Element; // @public (undocumented) export const isNewRelicDashboardAvailable: (entity: Entity) => boolean; diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index ba12a51f71..52a38c5677 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic-dashboard", - "version": "0.2.14", + "version": "0.2.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,6 +36,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -46,8 +47,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/dev-utils": "workspace:^", - "@testing-library/jest-dom": "^5.10.1", - "@types/react": "^16.13.1 || ^17.0.0" + "@testing-library/jest-dom": "^5.10.1" }, "files": [ "dist" diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index 4810cb41fb..e01be13fb3 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-newrelic +## 0.3.40-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + ## 0.3.39 ### Patch Changes diff --git a/plugins/newrelic/api-report.md b/plugins/newrelic/api-report.md index 302b7c918a..04fd229465 100644 --- a/plugins/newrelic/api-report.md +++ b/plugins/newrelic/api-report.md @@ -6,10 +6,11 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const NewRelicPage: () => JSX.Element; +export const NewRelicPage: () => JSX_2.Element; // @public (undocumented) const newRelicPlugin: BackstagePlugin< diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 7e49f1220b..3bcec5d62f 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-newrelic", "description": "A Backstage plugin that integrates towards New Relic", - "version": "0.3.39", + "version": "0.3.40-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,6 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "parse-link-header": "^2.0.0", "react-use": "^17.2.4" }, @@ -59,7 +60,6 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "@types/parse-link-header": "^2.0.1", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.2.3" }, diff --git a/plugins/nomad-backend/CHANGELOG.md b/plugins/nomad-backend/CHANGELOG.md index 235721a587..e7839f9dbb 100644 --- a/plugins/nomad-backend/CHANGELOG.md +++ b/plugins/nomad-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-nomad-backend +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + ## 0.1.2 ### Patch Changes diff --git a/plugins/nomad-backend/package.json b/plugins/nomad-backend/package.json index d1f3d4953e..e441bdb698 100644 --- a/plugins/nomad-backend/package.json +++ b/plugins/nomad-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-nomad-backend", - "version": "0.1.2", + "version": "0.1.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/nomad/CHANGELOG.md b/plugins/nomad/CHANGELOG.md index 951dc10174..9ddf4c0e11 100644 --- a/plugins/nomad/CHANGELOG.md +++ b/plugins/nomad/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-nomad +## 0.1.4-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/nomad/api-report.md b/plugins/nomad/api-report.md index f000854374..c2d06b9892 100644 --- a/plugins/nomad/api-report.md +++ b/plugins/nomad/api-report.md @@ -7,13 +7,14 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public -export const EntityNomadAllocationListTable: () => JSX.Element; +export const EntityNomadAllocationListTable: () => JSX_2.Element; // @public -export const EntityNomadJobVersionListCard: () => JSX.Element | null; +export const EntityNomadJobVersionListCard: () => JSX_2.Element | null; // @public (undocumented) export const isNomadAllocationsAvailable: (entity: Entity) => boolean; diff --git a/plugins/nomad/package.json b/plugins/nomad/package.json index 68bad74c0e..908dba4c14 100644 --- a/plugins/nomad/package.json +++ b/plugins/nomad/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-nomad", - "version": "0.1.2", + "version": "0.1.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,6 +37,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.60", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.3.0", "react-use": "^17.2.4" }, diff --git a/plugins/octopus-deploy/CHANGELOG.md b/plugins/octopus-deploy/CHANGELOG.md index eba4b64eda..eada829f0f 100644 --- a/plugins/octopus-deploy/CHANGELOG.md +++ b/plugins/octopus-deploy/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-octopus-deploy +## 0.2.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.2.3 ### Patch Changes diff --git a/plugins/octopus-deploy/api-report.md b/plugins/octopus-deploy/api-report.md index 53c92403bd..713d99aa8f 100644 --- a/plugins/octopus-deploy/api-report.md +++ b/plugins/octopus-deploy/api-report.md @@ -10,11 +10,12 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export const EntityOctopusDeployContent: (props: { defaultLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const isOctopusDeployAvailable: (entity: Entity) => boolean; diff --git a/plugins/octopus-deploy/package.json b/plugins/octopus-deploy/package.json index a715570fd0..f48ea06b19 100644 --- a/plugins/octopus-deploy/package.json +++ b/plugins/octopus-deploy/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-octopus-deploy", - "version": "0.2.3", + "version": "0.2.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,6 +36,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -53,7 +54,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/opencost/.eslintrc.js b/plugins/opencost/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/opencost/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/opencost/CHANGELOG.md b/plugins/opencost/CHANGELOG.md new file mode 100644 index 0000000000..1a4b7b4ecd --- /dev/null +++ b/plugins/opencost/CHANGELOG.md @@ -0,0 +1,15 @@ +# @backstage/plugin-opencost + +## 0.2.0-next.0 + +### Minor Changes + +- 1380a689ab56: New OpenCost plugin provides an port of the latest OpenCost UI to Backstage with updated dependencies. The plugin's README covers installation and configuration + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 diff --git a/plugins/opencost/README.md b/plugins/opencost/README.md new file mode 100644 index 0000000000..39c5e7ee2e --- /dev/null +++ b/plugins/opencost/README.md @@ -0,0 +1,79 @@ +# OpenCost + +Welcome to the [OpenCost](https://opencost.io) plugin! + +Currently this is a port of the [OpenCost UI](https://github.com/opencost/opencost/tree/develop/ui), but we will continue to expand it to expose any relevant data or pre-configured views that may be preferred. + +All of the code was originally ported from https://github.com/opencost/opencost/blob/develop/ui/ which is under by the Apache v2 License and also managed by the CNCF. + +## Installation + +1. Add the OpenCost dependency to the `packages/app/package.json`: + ```sh + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-opencost + ``` +2. Add the `OpenCostPage` to your `packages/app/src/App.tsx`: + + ```tsx + import { OpenCostPage } from '@backstage/plugin-opencost'; + ``` + +and + + ```tsx + + … + } /> + + ``` + +3. Add link to OpenCost to your sidebar + + ```typescript + // packages/app/src/components/Root/Root.tsx + import MoneyIcon from '@material-ui/icons/MonetizationOn'; + + ... + + export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + ... + + ... + + + ); + + ``` + +## Plugin Configuration + +Since OpenCost doesn't have any authentication at this point, you just need to give API access to the plugin to access your data. + +If you haven't set up an ingress rule, you can port-forward the API with + +``` +kubectl -n opencost port-forward deployment/opencost 9003 +``` + +Add the following to your `app-config.yaml`: + +```yaml +opencost: + baseUrl: http://localhost:9003 +``` + +## Ideas/Next Steps + +- More testing +- Use the OpenCost mascot for the sidebar logo +- Use the Backstage proxy to communicate with the OpenCost API if necessary for authentication +- Convert AllocationReport.js to use the [Backstage Table](https://backstage.io/storybook/?path=/story/data-display-table--default-table) +- Allow for user-provided default reports and/or disabling controls +- Support multiple hard-coded reports +- clean up deprecation warnings and upgrade to all the latest React components +- Fork(?) to support `Kubecost`, which could provide Alerts and Recommendations, similar to the Cost Explorer plugin + +![Screenshot](screenshot.png) diff --git a/plugins/opencost/api-report.md b/plugins/opencost/api-report.md new file mode 100644 index 0000000000..079f394618 --- /dev/null +++ b/plugins/opencost/api-report.md @@ -0,0 +1,23 @@ +## API Report File for "@backstage/plugin-opencost" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const OpenCostPage: () => JSX_2.Element; + +// @public (undocumented) +export const openCostPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {}, + {} +>; +``` diff --git a/plugins/opencost/config.d.ts b/plugins/opencost/config.d.ts new file mode 100644 index 0000000000..0070bec7b7 --- /dev/null +++ b/plugins/opencost/config.d.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + /** + * Configurations for the OpenCost plugin + * + * @visibility frontend + */ + opencost?: { + /** + * The base URL for the OpenCost API + * @visibility frontend + */ + baseUrl: string; + }; +} diff --git a/plugins/opencost/dev/index.tsx b/plugins/opencost/dev/index.tsx new file mode 100644 index 0000000000..41b2bd5346 --- /dev/null +++ b/plugins/opencost/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * 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 React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { openCostPlugin, OpenCostPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(openCostPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/opencost', + }) + .render(); diff --git a/plugins/opencost/package.json b/plugins/opencost/package.json new file mode 100644 index 0000000000..7e0b2282ca --- /dev/null +++ b/plugins/opencost/package.json @@ -0,0 +1,61 @@ +{ + "name": "@backstage/plugin-opencost", + "version": "0.2.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "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/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@date-io/luxon": "1.x", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.60", + "@material-ui/pickers": "^3.3.10", + "@material-ui/styles": "^4.11.5", + "@types/react": "^16.13.1 || ^17.0.0", + "axios": "^1.4.0", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "react-use": "^17.2.4", + "recharts": "^2.5.0" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "*" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/opencost/screenshot.png b/plugins/opencost/screenshot.png new file mode 100644 index 0000000000..432b525cb1 Binary files /dev/null and b/plugins/opencost/screenshot.png differ diff --git a/plugins/opencost/src/components/AllocationChart/RangeChart.jsx b/plugins/opencost/src/components/AllocationChart/RangeChart.jsx new file mode 100644 index 0000000000..b1ce5a34ee --- /dev/null +++ b/plugins/opencost/src/components/AllocationChart/RangeChart.jsx @@ -0,0 +1,201 @@ +/* + * 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 React from 'react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts'; +import { makeStyles } from '@material-ui/styles'; +import { reverse } from 'lodash'; +import { primary, greyscale, browns } from '../../constants/colors'; +import { toCurrency } from '../../util'; + +const useStyles = makeStyles({ + tooltip: { + borderRadius: 2, + background: 'rgba(255, 255, 255, 0.95)', + padding: 12, + }, + tooltipLineItem: { + fontSize: '1rem', + margin: 0, + marginBottom: 4, + padding: 0, + }, +}); + +function toBarLabels(allocationRange) { + const keyToFill = {}; + let p = 0; + let g = 0; + let b = 0; + + for (const { idle } of allocationRange) { + for (const allocation of idle) { + const key = allocation.name; + if (keyToFill[key] === undefined) { + // idle allocations are assigned grey + keyToFill[key] = greyscale[g]; + g = (g + 1) % greyscale.length; + } + } + } + + for (const { top } of allocationRange) { + for (const allocation of top) { + const key = allocation.name; + if (keyToFill[key] === undefined) { + if (key === '__unallocated__') { + // unallocated gets black (clean up) + keyToFill[key] = '#212121'; + } else { + // non-idle allocations get the next available color + keyToFill[key] = primary[p]; + p = (p + 1) % primary.length; + } + } + } + } + + for (const { other } of allocationRange) { + for (const allocation of other) { + const key = allocation.name; + if (keyToFill[key] === undefined) { + // idle allocations are assigned grey + keyToFill[key] = browns[b]; + b = (b + 1) % browns.length; + } + } + } + + const labels = []; + for (const key in keyToFill) { + if (Object.hasOwn(keyToFill, key)) { + labels.push({ + dataKey: key, + fill: keyToFill[key], + }); + } + } + + return reverse(labels); +} + +function toBar(datum) { + const { top, other, idle } = datum; + const bar = {}; + + for (const key in top) { + if (Object.hasOwn(top, key)) { + const allocation = top[key]; + const start = new Date(allocation.start); + bar.start = `${start.getUTCFullYear()}-${ + start.getUTCMonth() + 1 + }-${start.getUTCDate()}`; + bar[allocation.name] = allocation.totalCost; + } + } + + for (const key in other) { + if (Object.hasOwn(other, key)) { + const allocation = other[key]; + const start = new Date(allocation.start); + bar.start = `${start.getUTCFullYear()}-${ + start.getUTCMonth() + 1 + }-${start.getUTCDate()}`; + bar[allocation.name] = allocation.totalCost; + } + } + + for (const key in idle) { + if (Object.hasOwn(idle, key)) { + const allocation = idle[key]; + const start = new Date(allocation.start); + bar.start = `${start.getUTCFullYear()}-${ + start.getUTCMonth() + 1 + }-${start.getUTCDate()}`; + bar[allocation.name] = allocation.totalCost; + } + } + + return bar; +} + +const RangeChart = ({ data, currency, height }) => { + const classes = useStyles(); + + const barData = data.map(toBar); + const barLabels = toBarLabels(data); + + const CustomTooltip = params => { + const { active, payload } = params; + + if (!payload || payload.length === 0) { + return null; + } + + const total = payload.reduce((sum, item) => sum + item.value, 0.0); + if (active) { + /* eslint react/forbid-elements: [0, { allow: ["warning"] }] */ + return ( +
+

{`Total: ${toCurrency(total, currency)}`}

+ {reverse(payload).map((item, i) => ( +

{`${item.name}: ${toCurrency(item.value, currency)}`}

+ ))} +
+ ); + } + + return null; + }; + + return ( + + + + + + } /> + {barLabels.map((barLabel, i) => ( + + ))} + + + ); +}; + +export default RangeChart; diff --git a/plugins/opencost/src/components/AllocationChart/SummaryChart.jsx b/plugins/opencost/src/components/AllocationChart/SummaryChart.jsx new file mode 100644 index 0000000000..53c0842845 --- /dev/null +++ b/plugins/opencost/src/components/AllocationChart/SummaryChart.jsx @@ -0,0 +1,127 @@ +/* + * 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 React from 'react'; +import { ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; +import { primary, greyscale, browns } from '../../constants/colors'; +import { toCurrency } from '../../util'; + +function toPieData(top, other, idle) { + const slices = []; + + for (const i in top) { + if (Object.hasOwn(top, i)) { + const allocation = top[i]; + const fill = + allocation.name === '__unallocated__' + ? '#212121' + : primary[i % primary.length]; + + slices.push({ + name: allocation.name, + value: allocation.totalCost, + fill: fill, + }); + } + } + + for (const i in other) { + if (Object.hasOwn(other, i)) { + const allocation = other[i]; + const fill = browns[i % browns.length]; + slices.push({ + name: allocation.name, + value: allocation.totalCost, + fill: fill, + }); + } + } + + for (const i in idle) { + if (Object.hasOwn(idle, i)) { + const allocation = idle[i]; + const fill = greyscale[i % greyscale.length]; + slices.push({ + name: allocation.name, + value: allocation.totalCost, + fill: fill, + }); + } + } + + return slices; +} + +const SummaryChart = ({ top, other, idle, currency, height }) => { + const pieData = toPieData(top, other, idle); + + const renderLabel = params => { + const { cx, cy, midAngle, outerRadius, percent, name, fill, value } = + params; + + const RADIAN = Math.PI / 180; + const radius = outerRadius * 1.1; + let x = cx + radius * Math.cos(-midAngle * RADIAN); + x += x > cx ? 2 : -2; + const y = cy + radius * Math.sin(-midAngle * RADIAN); + // y -= Math.min(Math.abs(2 / Math.cos(-midAngle * RADIAN)), 8) + + if (percent < 0.02) { + return undefined; + } + + return ( + cx ? 'start' : 'end'} + dominantBaseline="central" + > + {`${name}: ${toCurrency(value, currency)} (${(percent * 100).toFixed( + 1, + )}%)`} + + ); + }; + + return ( + + + + {pieData.map((datum, i) => ( + + ))} + + + + ); +}; + +export default SummaryChart; diff --git a/plugins/opencost/src/components/AllocationChart/index.jsx b/plugins/opencost/src/components/AllocationChart/index.jsx new file mode 100644 index 0000000000..9fec3e296f --- /dev/null +++ b/plugins/opencost/src/components/AllocationChart/index.jsx @@ -0,0 +1,108 @@ +/* + * 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 React from 'react'; +import { isArray, filter, map, reduce, reverse, sortBy } from 'lodash'; + +import Typography from '@material-ui/core/Typography'; + +import RangeChart from './RangeChart'; +import SummaryChart from './SummaryChart'; + +// TODO niko/etl +/* eslint @typescript-eslint/no-shadow: ["error", { "allow": ["agg"] }]*/ +function agg(allocationSet, name) { + if (allocationSet.length === 0) { + return null; + } + + return reduce( + allocationSet, + (agg, cur) => ({ + name: agg.name, + aggregatedBy: cur.aggregatedBy, + properties: agg.properties, + start: cur.start, + end: cur.end, + cpuCost: agg.cpuCost + cur.cpuCost, + gpuCost: agg.gpuCost + cur.gpuCost, + ramCost: agg.ramCost + cur.ramCost, + pvCost: agg.pvCost + cur.pvCost, + totalCost: agg.totalCost + cur.totalCost, + count: agg.count + 1, + }), + { + name: name, + properties: null, + cpuCost: 0.0, + gpuCost: 0.0, + ramCost: 0.0, + pvCost: 0.0, + totalCost: 0.0, + count: 0, + }, + ); +} + +function isIdle(allocation) { + return allocation.name.indexOf('__idle__') >= 0; +} + +function top(n, by) { + return allocations => { + if (isArray(allocations[0])) { + return map(allocations, top(n, by)); + } + + const sorted = reverse(sortBy(allocations, by)); + const active = filter(sorted, a => !isIdle(a)); + const idle = filter(sorted, a => isIdle(a)); + const topn = active.slice(0, n); + const other = []; + if (active.length > n) { + other.push(agg(active.slice(n), 'other')); + } + + return { + top: topn, + other: other, + idle: idle, + }; + }; +} + +const AllocationChart = ({ allocationRange, currency, n, height }) => { + if (allocationRange.length === 0) { + return No data; + } + + if (allocationRange.length === 1) { + const datum = top(n, alloc => alloc.totalCost)(allocationRange[0]); + return ( + + ); + } + + const data = top(n, alloc => alloc.totalCost)(allocationRange); + return ; +}; + +export default React.memo(AllocationChart); diff --git a/plugins/opencost/src/components/AllocationReport.jsx b/plugins/opencost/src/components/AllocationReport.jsx new file mode 100644 index 0000000000..52686a741c --- /dev/null +++ b/plugins/opencost/src/components/AllocationReport.jsx @@ -0,0 +1,261 @@ +/* not changing the working code at this point */ +/* eslint no-nested-ternary: 0 */ +/* eslint react-hooks/rules-of-hooks: 0 */ + +/* + * 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 React, { useEffect, useState } from 'react'; +import { get, round } from 'lodash'; +import { makeStyles } from '@material-ui/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TablePagination from '@material-ui/core/TablePagination'; +import TableRow from '@material-ui/core/TableRow'; +import TableSortLabel from '@material-ui/core/TableSortLabel'; +import Typography from '@material-ui/core/Typography'; +import AllocationChart from './AllocationChart'; +import { toCurrency } from '../util'; + +const useStyles = makeStyles({ + noResults: { + padding: 24, + }, +}); + +function descendingComparator(a, b, orderBy) { + if (get(b, orderBy) < get(a, orderBy)) { + return -1; + } + if (get(b, orderBy) > get(a, orderBy)) { + return 1; + } + return 0; +} + +function getComparator(order, orderBy) { + return order === 'desc' + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + stabilizedThis.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) return order; + return a[1] - b[1]; + }); + return stabilizedThis.map(el => el[0]); +} + +const headCells = [ + { id: 'name', numeric: false, label: 'Name', width: 'auto' }, + { id: 'cpuCost', numeric: true, label: 'CPU', width: 100 }, + { id: 'ramCost', numeric: true, label: 'RAM', width: 100 }, + { id: 'pvCost', numeric: true, label: 'PV', width: 100 }, + { id: 'totalEfficiency', numeric: true, label: 'Efficiency', width: 130 }, + { id: 'totalCost', numeric: true, label: 'Total cost', width: 130 }, +]; + +const AllocationReport = ({ + allocationData, + cumulativeData, + totalData, + currency, +}) => { + const classes = useStyles(); + + if (allocationData.length === 0) { + return ( + + No results + + ); + } + + const [order, setOrder] = React.useState('desc'); + const [orderBy, setOrderBy] = React.useState('totalCost'); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(25); + const numData = cumulativeData.length; + + useEffect(() => { + setPage(0); + }, [numData]); + + const lastPage = Math.floor(numData / rowsPerPage); + + const handleChangePage = (event, newPage) => setPage(newPage); + + const handleChangeRowsPerPage = event => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const handleRequestSort = (event, property) => { + const isDesc = orderBy === property && order === 'desc'; + setOrder(isDesc ? 'asc' : 'desc'); + setOrderBy(property); + }; + + const createSortHandler = property => event => + handleRequestSort(event, property); + + const orderedRows = stableSort(cumulativeData, getComparator(order, orderBy)); + const pageRows = orderedRows.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage, + ); + + return ( +
+ + +
+ + + +
+ + + {headCells.map(cell => ( + + + {cell.label} + + + ))} + + + + + {headCells.map(cell => { + return ( + + {cell.numeric + ? cell.label === 'Efficiency' + ? totalData.totalEfficiency === 1.0 && + totalData.cpuReqCoreHrs === 0 && + totalData.ramReqByteHrs === 0 + ? 'Inf%' + : `${round(totalData.totalEfficiency * 100, 1)}%` + : toCurrency(totalData[cell.id], currency) + : totalData[cell.id]} + + ); + })} + + {pageRows.map((row, key) => { + if (row.name === '__unmounted__') { + row.name = 'Unmounted PVs'; + } + + const isIdle = row.name.indexOf('__idle__') >= 0; + const isUnallocated = row.name.indexOf('__unallocated__') >= 0; + const isUnmounted = row.name.indexOf('Unmounted PVs') >= 0; + + // Replace "efficiency" with Inf if there is usage w/o request + let efficiency = round(row.totalEfficiency * 100, 1); + if ( + row.totalEfficiency === 1.0 && + row.cpuReqCoreHrs === 0 && + row.ramReqByteHrs === 0 + ) { + efficiency = 'Inf'; + } + + // Do not allow drill-down for idle and unallocated rows + if (isIdle || isUnallocated || isUnmounted) { + return ( + + {row.name} + + {toCurrency(row.cpuCost, currency)} + + + {toCurrency(row.ramCost, currency)} + + + {toCurrency(row.pvCost, currency)} + + {isIdle ? ( + + ) : ( + {efficiency}% + )} + + {toCurrency(row.totalCost, currency)} + + + ); + } + + return ( + + {row.name} + + {toCurrency(row.cpuCost, currency)} + + + {toCurrency(row.ramCost, currency)} + + + {toCurrency(row.pvCost, currency)} + + {efficiency}% + + {toCurrency(row.totalCost, currency)} + + + ); + })} + +
+ + +
+ ); +}; + +export default React.memo(AllocationReport); diff --git a/plugins/opencost/src/components/Controls/Download.jsx b/plugins/opencost/src/components/Controls/Download.jsx new file mode 100644 index 0000000000..727188e5c1 --- /dev/null +++ b/plugins/opencost/src/components/Controls/Download.jsx @@ -0,0 +1,110 @@ +/* + * 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 React from 'react'; +import { get, forEach, reverse, round, sortBy } from 'lodash'; +import ExportIcon from '@material-ui/icons/GetApp'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; + +const columns = [ + { + head: 'Name', + prop: 'name', + currency: false, + }, + { + head: 'CPU', + prop: 'cpuCost', + currency: true, + }, + { + head: 'GPU', + prop: 'gpuCost', + currency: true, + }, + { + head: 'RAM', + prop: 'ramCost', + currency: true, + }, + { + head: 'PV', + prop: 'pvCost', + currency: true, + }, + { + head: 'Network', + prop: 'networkCost', + currency: true, + }, + { + head: 'Shared', + prop: 'sharedCost', + currency: true, + }, + { + head: 'Total', + prop: 'totalCost', + currency: true, + }, +]; + +const toCSVLine = datum => { + const cols = []; + + forEach(columns, c => { + if (c.currency) { + cols.push(round(get(datum, c.prop, 0.0), 2)); + } else { + cols.push(`"${get(datum, c.prop, '')}"`); + } + }); + + return cols.join(','); +}; + +const DownloadControl = ({ cumulativeData, title }) => { + // downloadReport downloads a CSV of the cumulative allocation data + function downloadReport() { + // Build CSV + const head = columns.map(c => c.head).join(','); + const body = reverse(sortBy(cumulativeData, 'totalCost')) + .map(toCSVLine) + .join('\r\n'); + const csv = `${head}\r\n${body}`; + + // Create download link + const a = document.createElement('a'); + a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' })); + const filename = title.toLocaleLowerCase('en-US').replace(/\s/gi, '-'); + a.setAttribute('download', `${filename}-${Date.now()}.csv`); + + // Click the link + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + + return ( + + + + + + ); +}; + +export default React.memo(DownloadControl); diff --git a/plugins/opencost/src/components/Controls/Edit.jsx b/plugins/opencost/src/components/Controls/Edit.jsx new file mode 100644 index 0000000000..f02f8cc111 --- /dev/null +++ b/plugins/opencost/src/components/Controls/Edit.jsx @@ -0,0 +1,106 @@ +/* + * 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 { makeStyles } from '@material-ui/styles'; +import FormControl from '@material-ui/core/FormControl'; +import InputLabel from '@material-ui/core/InputLabel'; +import MenuItem from '@material-ui/core/MenuItem'; +import Select from '@material-ui/core/Select'; + +import React from 'react'; + +import SelectWindow from '../SelectWindow'; + +const useStyles = makeStyles({ + wrapper: { + display: 'inline-flex', + }, + formControl: { + margin: 8, + minWidth: 120, + }, +}); + +function EditControl({ + windowOptions, + window, + setWindow, + aggregationOptions, + aggregateBy, + setAggregateBy, + accumulateOptions, + accumulate, + setAccumulate, + currencyOptions, + currency, + setCurrency, +}) { + const classes = useStyles(); + return ( +
+ + + Breakdown + + + + Resolution + + + + Currency + + +
+ ); +} + +export default React.memo(EditControl); diff --git a/plugins/opencost/src/components/Controls/index.jsx b/plugins/opencost/src/components/Controls/index.jsx new file mode 100644 index 0000000000..c63833a6ab --- /dev/null +++ b/plugins/opencost/src/components/Controls/index.jsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; +import DownloadControl from './Download'; +import EditControl from './Edit'; + +const Controls = ({ + windowOptions, + window, + setWindow, + aggregationOptions, + aggregateBy, + setAggregateBy, + accumulateOptions, + accumulate, + setAccumulate, + title, + cumulativeData, + currency, + currencyOptions, + setCurrency, +}) => { + return ( +
+ + +
+ ); +}; + +export default React.memo(Controls); diff --git a/plugins/opencost/src/components/Details.js b/plugins/opencost/src/components/Details.js new file mode 100644 index 0000000000..5245c9e9fa --- /dev/null +++ b/plugins/opencost/src/components/Details.js @@ -0,0 +1,287 @@ +/* + * 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 React, { memo, useEffect, useState } from 'react'; +import { forEach, get, reverse, round, sortBy } from 'lodash'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import ClusterIcon from '@material-ui/icons/GroupWork'; +import NodeIcon from '@material-ui/icons/Memory'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Warnings from './Warnings'; +import AllocationService from '../services/allocation'; +import { bytesToString, toCurrency } from '../util'; + +const Details = ({ + window, + namespace, + controllerKind, + controller, + pod, + currency, +}) => { + const [cluster, setCluster] = useState(''); + const [node, setNode] = useState(''); + + const [fetch, setFetch] = useState(true); + const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState([]); + const [rows, setRows] = useState([]); + + useEffect(() => { + if (fetch) { + setCluster(''); + setNode(''); + fetchData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetch]); + + async function fetchData() { + setLoading(true); + setErrors([]); + + try { + const filters = []; + + if (cluster) { + filters.push({ + property: 'cluster', + value: cluster, + }); + } + + if (node) { + filters.push({ + property: 'node', + value: node, + }); + } + + if (namespace) { + filters.push({ + property: 'namespace', + value: namespace, + }); + } + + if (controllerKind) { + filters.push({ + property: 'controllerKind', + value: controllerKind, + }); + } + + if (controller) { + filters.push({ + property: 'controller', + value: controller, + }); + } + + if (pod) { + filters.push({ + property: 'pod', + value: pod, + }); + } + + const resp = await AllocationService.fetchAllocation(window, '', { + accumulate: true, + }); + + let data = []; + forEach(resp.data[0], datum => { + if (datum.name === '__idle__') { + return; + } + + if (!cluster) { + setCluster(get(datum, 'properties.cluster', '')); + } + + if (!node) { + setNode(get(datum, 'properties.node', '')); + } + + // TODO can we get pod, container back in properties? + const names = datum.name.split('/'); + datum.pod = names[names.length - 2]; + datum.container = names[names.length - 1]; + + datum.hours = round(get(datum, 'minutes', 0.0) / 60.0, 2); + + if (datum.hours > 0) { + datum.cpu = round(get(datum, 'cpuCoreHours', 0.0) / datum.hours, 2); + datum.cpuCostPerCoreHr = datum.cpuCost / (datum.cpu * datum.hours); + if (datum.cpu === 0) { + datum.cpuCostPerCoreHr = 0.0; + } + + datum.ram = round(get(datum, 'ramByteHours', 0.0) / datum.hours, 2); + const ramGiB = datum.ram / 1024 / 1024 / 1024; + datum.ramCostPerGiBHr = datum.ramCost / (ramGiB * datum.hours); + if (ramGiB === 0) { + datum.ramCostPerGiBHr = 0.0; + } + } else { + datum.cpu = 0.0; + datum.cpuCostPerCoreHr = 0.0; + datum.ram = 0.0; + datum.ramCostPerGiBHr = 0.0; + } + + data.push(datum); + }); + + data = reverse(sortBy(data, 'totalCost')); + + setRows(data); + } catch (e) { + /* eslint no-console: ["error", { allow: ["warn"] }] */ + console.warn( + `Error fetching details for (${controllerKind}, ${controller}):`, + e, + ); + setErrors([ + { + primary: 'Error fetching details', + secondary: `Tried fetching details for: ${namespace}, ${controllerKind}, ${controller}, ${pod}`, + }, + ]); + } + + setLoading(false); + setFetch(false); + } + + if (loading) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+ {!loading && errors.length > 0 && ( +
+ +
+ )} + + + {cluster && ( + + + + + + + )} + {node && ( + + + + + + + )} + + + + + + + Container + + + Hours + + + CPU + + + $/(CPU*Hr) + + + CPU cost + + + RAM + + + $/(GiB*Hr) + + + RAM cost + + + Total cost + + + + + {rows.map((row, i) => ( + + + {row.container} + + + {row.hours} + + + {row.cpu} + + + {toCurrency(row.cpuCostPerCoreHr, currency, 5)} + + + {toCurrency(row.cpuCost, currency, 3)} + + + {bytesToString(row.ram)} + + + {toCurrency(row.ramCostPerGiBHr, currency, 5)} + + + {toCurrency(row.ramCost, currency, 3)} + + + {toCurrency(row.totalCost, currency, 3)} + + + ))} + +
+
+
+ ); +}; + +export default memo(Details); diff --git a/plugins/opencost/src/components/OpenCostPage/OpenCostPage.tsx b/plugins/opencost/src/components/OpenCostPage/OpenCostPage.tsx new file mode 100644 index 0000000000..bd60fa0c35 --- /dev/null +++ b/plugins/opencost/src/components/OpenCostPage/OpenCostPage.tsx @@ -0,0 +1,44 @@ +/* + * 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 React from 'react'; +import { Grid } from '@material-ui/core'; +import { Header, Page, Content } from '@backstage/core-components'; +import { OpenCostReport } from '../OpenCostReport'; + +export const OpenCostPage = () => ( + +
+ + OpenCost + +
+ + + + + + + +
+); diff --git a/plugins/opencost/src/components/OpenCostPage/index.ts b/plugins/opencost/src/components/OpenCostPage/index.ts new file mode 100644 index 0000000000..f966c83a30 --- /dev/null +++ b/plugins/opencost/src/components/OpenCostPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { OpenCostPage } from './OpenCostPage'; diff --git a/plugins/opencost/src/components/OpenCostReport/OpenCostReport.tsx b/plugins/opencost/src/components/OpenCostReport/OpenCostReport.tsx new file mode 100644 index 0000000000..12f64f3606 --- /dev/null +++ b/plugins/opencost/src/components/OpenCostReport/OpenCostReport.tsx @@ -0,0 +1,319 @@ +/* + * 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. + */ +// code ported from https://github.com/opencost/opencost/blob/develop/ui/src/Reports.js + +import React, { useEffect, useState } from 'react'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import IconButton from '@material-ui/core/IconButton'; +import Paper from '@material-ui/core/Paper'; +import RefreshIcon from '@material-ui/icons/Refresh'; +import Typography from '@material-ui/core/Typography'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { find, get, sortBy, toArray } from 'lodash'; +import { makeStyles } from '@material-ui/styles'; +import AllocationReport from '../AllocationReport'; +import AllocationService from '../../services/allocation'; +import Controls from '../Controls'; +import Page from '../Page'; +import Subtitle from '../Subtitle'; +import Warnings from '../Warnings'; +import { + checkCustomWindow, + cumulativeToTotals, + rangeToCumulative, + toVerboseTimeRange, +} from '../../util'; +import { currencyCodes } from '../../constants/currencyCodes'; +import { useApi, configApiRef } from '@backstage/core-plugin-api'; + +const windowOptions = [ + { name: 'Today', value: 'today' }, + { name: 'Yesterday', value: 'yesterday' }, + { name: 'Week-to-date', value: 'week' }, + { name: 'Month-to-date', value: 'month' }, + { name: 'Last week', value: 'lastweek' }, + { name: 'Last month', value: 'lastmonth' }, + { name: 'Last 7 days', value: '6d' }, + { name: 'Last 30 days', value: '29d' }, + { name: 'Last 60 days', value: '59d' }, + { name: 'Last 90 days', value: '89d' }, +]; + +const aggregationOptions = [ + { name: 'Cluster', value: 'cluster' }, + { name: 'Node', value: 'node' }, + { name: 'Namespace', value: 'namespace' }, + { name: 'Controller kind', value: 'controllerKind' }, + { name: 'Controller', value: 'controller' }, + { name: 'Service', value: 'service' }, + { name: 'Pod', value: 'pod' }, + { name: 'Container', value: 'container' }, +]; + +const accumulateOptions = [ + { name: 'Entire window', value: true }, + { name: 'Daily', value: false }, +]; + +const useStyles = makeStyles({ + reportHeader: { + display: 'flex', + flexFlow: 'row', + padding: 24, + }, + titles: { + flexGrow: 1, + }, +}); + +// generateTitle generates a string title from a report object +// @ts-ignore: implicitly has an 'any' type +function generateTitle({ window, aggregateBy, accumulate }) { + let windowName = get(find(windowOptions, { value: window }), 'name', ''); + if (windowName === '') { + if (checkCustomWindow(window)) { + windowName = toVerboseTimeRange(window); + } else { + /* eslint no-console: ["error", { allow: ["warn"] }] */ + console.warn(`unknown window: ${window}`); + } + } + + const aggregationName = get( + find(aggregationOptions, { value: aggregateBy }), + 'name', + '', + ).toLocaleLowerCase('en-US'); + if (aggregationName === '') { + /* eslint no-console: ["error", { allow: ["warn"] }] */ + console.warn(`unknown aggregation: ${aggregateBy}`); + } + + let str = `${windowName} by ${aggregationName}`; + + if (!accumulate) { + str = `${str} daily`; + } + + return str; +} + +export const OpenCostReport = () => { + const classes = useStyles(); + // Allocation data state + const [allocationData, setAllocationData] = useState([]); + const [cumulativeData, setCumulativeData] = useState({}); + const [totalData, setTotalData] = useState({}); + + // When allocation data changes, create a cumulative version of it + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const cumulative = rangeToCumulative(allocationData, aggregateBy); + setCumulativeData(toArray(cumulative)); + setTotalData(cumulativeToTotals(cumulative)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allocationData]); + + // Form state, which controls form elements, but not the report itself. On + // certain actions, the form state may flow into the report state. + const [window, setWindow] = useState(windowOptions[0].value); + const [aggregateBy, setAggregateBy] = useState(aggregationOptions[0].value); + const [accumulate, setAccumulate] = useState(accumulateOptions[0].value); + const [currency, setCurrency] = useState('USD'); + + // Report state, including current report and saved options + const [title, setTitle] = useState('Last 7 days by namespace daily'); + + // When parameters changes, fetch data. This should be the + // only mechanism used to fetch data. Also generate a sensible title from the paramters. + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + setFetch(true); + setTitle(generateTitle({ window, aggregateBy, accumulate })); + }, [window, aggregateBy, accumulate]); + + // page and settings state + const [init, setInit] = useState(false); + const [fetch, setFetch] = useState(false); + const [loading, setLoading] = useState(true); + const [errors, setErrors] = useState([]); + + // Initialize once, then fetch report each time setFetch(true) is called + useEffect(() => { + if (!init) { + initialize(); + } + if (init && fetch) { + fetchData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [init, fetch]); + + // parse any context information from the URL + const routerLocation = useLocation(); + const searchParams = new URLSearchParams(routerLocation.search); + const routerNavigate = useNavigate(); + useEffect(() => { + setWindow(searchParams.get('window') || '6d'); + setAggregateBy(searchParams.get('agg') || 'namespace'); + setAccumulate(searchParams.get('acc') === 'true' || false); + setCurrency(searchParams.get('currency') || 'USD'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [routerLocation]); + + async function initialize() { + setInit(true); + } + + const configApi = useApi(configApiRef); + const baseUrl = configApi.getConfig('opencost').getString('baseUrl'); + /* eslint no-console: 0 */ + console.log(`baseUrl:${baseUrl}`); + + async function fetchData() { + setLoading(true); + setErrors([]); + + try { + const resp = await AllocationService.fetchAllocation( + baseUrl, + window, + aggregateBy, + { accumulate }, + ); + if (resp.data && resp.data.length > 0) { + const allocationRange = resp.data; + for (const i in allocationRange) { + if (Object.hasOwn(allocationRange, i)) { + // update cluster aggregations to use clusterName/clusterId names + allocationRange[i] = sortBy(allocationRange[i], a => a.totalCost); + } + } + setAllocationData(allocationRange); + } else { + setAllocationData([]); + } + } catch (err) { + if (err.message.indexOf('404') === 0) { + setErrors([ + // @ts-ignore: not assignable to type 'never' + { + primary: 'Failed to load report data', + secondary: + 'Please update OpenCost to the latest version, then open an Issue on GitHub if problems persist.', + }, + ]); + } else { + let secondary = + 'Please open an OpenCost issue with a bug report if problems persist.'; + if (err.message.length > 0) { + secondary = err.message; + } + setErrors([ + // @ts-ignore: not assignable to type 'never' + { + primary: 'Failed to load report data', + secondary: secondary, + }, + ]); + } + setAllocationData([]); + } + + setLoading(false); + setFetch(false); + } + return ( + + {!loading && errors.length > 0 && ( +
+ +
+ )} + {init && ( + +
+
+ {title} + +
+ + setFetch(true)}> + + + + { + searchParams.set('window', win); + routerNavigate({ + search: `?${searchParams.toString()}`, + }); + }} + aggregationOptions={aggregationOptions} + aggregateBy={aggregateBy} + // @ts-ignore: implicitly has an 'any' type + setAggregateBy={agg => { + searchParams.set('agg', agg); + routerNavigate({ + search: `?${searchParams.toString()}`, + }); + }} + accumulateOptions={accumulateOptions} + accumulate={accumulate} + // @ts-ignore: implicitly has an 'any' type + setAccumulate={acc => { + searchParams.set('acc', acc); + routerNavigate({ + search: `?${searchParams.toString()}`, + }); + }} + title={title} + cumulativeData={cumulativeData} + currency={currency} + currencyOptions={currencyCodes} + // @ts-ignore: implicitly has an 'any' type + setCurrency={curr => { + searchParams.set('currency', curr); + routerNavigate({ + search: `?${searchParams.toString()}`, + }); + }} + /> +
+ + {loading && ( +
+
+ +
+
+ )} + {!loading && ( + + )} +
+ )} +
+ ); +}; diff --git a/plugins/opencost/src/components/OpenCostReport/index.ts b/plugins/opencost/src/components/OpenCostReport/index.ts new file mode 100644 index 0000000000..aee85ea8db --- /dev/null +++ b/plugins/opencost/src/components/OpenCostReport/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { OpenCostReport } from './OpenCostReport'; diff --git a/plugins/opencost/src/components/Page.jsx b/plugins/opencost/src/components/Page.jsx new file mode 100644 index 0000000000..6346b6287b --- /dev/null +++ b/plugins/opencost/src/components/Page.jsx @@ -0,0 +1,47 @@ +/* + * 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 { makeStyles } from '@material-ui/styles'; +import React from 'react'; + +const useStyles = makeStyles({ + wrapper: { + display: 'flex', + flexFlow: 'column', + flexGrow: 1, + margin: '20px 30px 0 30px', + minWidth: 800, + }, + flexGrow: { + display: 'flex', + flexFlow: 'column', + flexGrow: 1, + }, +}); + +const Page = props => { + const classes = useStyles(); + + return ( +
+
+
{props.children}
+
+
+ ); +}; + +export default Page; diff --git a/plugins/opencost/src/components/SelectWindow.jsx b/plugins/opencost/src/components/SelectWindow.jsx new file mode 100644 index 0000000000..e374571c00 --- /dev/null +++ b/plugins/opencost/src/components/SelectWindow.jsx @@ -0,0 +1,214 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { makeStyles } from '@material-ui/styles'; +import { endOfDay, startOfDay } from '@date-io/luxon'; +import { + MuiPickersUtilsProvider, + KeyboardDatePicker, +} from '@material-ui/pickers'; +import Button from '@material-ui/core/Button'; +import LuxonUtils from '@date-io/luxon'; +import FormControl from '@material-ui/core/FormControl'; +import Link from '@material-ui/core/Link'; +import Popover from '@material-ui/core/Popover'; +import TextField from '@material-ui/core/TextField'; +import Typography from '@material-ui/core/Typography'; +import { isValid } from 'date-fns'; +import { find, get } from 'lodash'; + +const useStyles = makeStyles({ + dateContainer: { + paddingLeft: 18, + paddingRight: 18, + paddingTop: 6, + paddingBottom: 18, + display: 'flex', + flexFlow: 'row', + }, + dateContainerColumn: { + display: 'flex', + flexFlow: 'column', + }, + formControl: { + margin: 8, + width: 120, + }, +}); + +const SelectWindow = ({ windowOptions, window, setWindow }) => { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(null); + + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + const [intervalString, setIntervalString] = useState(null); + + const handleClick = event => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleStartDateChange = date => { + if (isValid(date)) { + setStartDate(startOfDay(date)); + } + }; + + const handleEndDateChange = date => { + if (isValid(date)) { + setEndDate(endOfDay(date)); + } + }; + + const handleSubmitPresetDates = dateString => { + setWindow(dateString); + setStartDate(null); + setEndDate(null); + handleClose(); + }; + + const handleSubmitCustomDates = () => { + if (intervalString !== null) { + setWindow(intervalString); + handleClose(); + } + }; + + useEffect(() => { + if (startDate !== null && endDate !== null) { + // Note: getTimezoneOffset() is calculated based on current system locale, NOT date object + const adjustedStartDate = new Date( + startDate - startDate.getTimezoneOffset() * 60000, + ); + const adjustedEndDate = new Date( + endDate - endDate.getTimezoneOffset() * 60000, + ); + setIntervalString( + `${adjustedStartDate.toISOString().split('.')[0]}Z` + + `,${adjustedEndDate.toISOString().split('.')[0]}Z`, + ); + } + }, [startDate, endDate]); + + const open = Boolean(anchorEl); + const id = open ? 'date-range-popover' : undefined; + + return ( + <> + + handleClick(e)} + inputProps={{ + readOnly: true, + style: { cursor: 'pointer' }, + }} + /> + + +
+
+ + + + +
+ +
+
+
+ {windowOptions.map(opt => ( + + handleSubmitPresetDates(opt.value)} + > + {opt.name} + + + ))} +
+
+
+ + ); +}; + +export default React.memo(SelectWindow); diff --git a/plugins/opencost/src/components/Subtitle.jsx b/plugins/opencost/src/components/Subtitle.jsx new file mode 100644 index 0000000000..63b3df61ff --- /dev/null +++ b/plugins/opencost/src/components/Subtitle.jsx @@ -0,0 +1,59 @@ +/* + * 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 React from 'react'; +import { makeStyles } from '@material-ui/styles'; +import { upperFirst } from 'lodash'; +import Breadcrumbs from '@material-ui/core/Breadcrumbs'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import Typography from '@material-ui/core/Typography'; +import { toVerboseTimeRange } from '../util'; + +const useStyles = makeStyles({ + root: { + '& > * + *': { + marginTop: 2, + }, + }, + link: { + cursor: 'pointer', + }, +}); + +const Subtitle = ({ report }) => { + const classes = useStyles(); + + const { aggregateBy, window } = report; + + return ( +
+ } + aria-label="breadcrumb" + > + {aggregateBy && aggregateBy.length > 0 ? ( + + {toVerboseTimeRange(window)} by {upperFirst(aggregateBy)} + + ) : ( + {toVerboseTimeRange(window)} + )} + +
+ ); +}; + +export default React.memo(Subtitle); diff --git a/plugins/opencost/src/components/Warnings.jsx b/plugins/opencost/src/components/Warnings.jsx new file mode 100644 index 0000000000..ae1047ebc2 --- /dev/null +++ b/plugins/opencost/src/components/Warnings.jsx @@ -0,0 +1,53 @@ +/* + * 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 React from 'react'; +import { makeStyles } from '@material-ui/styles'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import Paper from '@material-ui/core/Paper'; +import WarningIcon from '@material-ui/icons/Warning'; + +const useStyles = makeStyles({ + root: {}, +}); + +const Warnings = ({ warnings }) => { + const classes = useStyles(); + + if (!warnings || warnings.length === 0) { + return null; + } + + return ( + + + {warnings.map((warn, i) => ( + + + + + + + ))} + + + ); +}; + +export default Warnings; diff --git a/plugins/opencost/src/constants/colors.js b/plugins/opencost/src/constants/colors.js new file mode 100644 index 0000000000..8e0289541b --- /dev/null +++ b/plugins/opencost/src/constants/colors.js @@ -0,0 +1,52 @@ +/* + * 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 blue from '@material-ui/core/colors/blue'; +import brown from '@material-ui/core/colors/brown'; +import cyan from '@material-ui/core/colors/cyan'; +import deepOrange from '@material-ui/core/colors/deepOrange'; +import deepPurple from '@material-ui/core/colors/deepPurple'; +import green from '@material-ui/core/colors/green'; +import grey from '@material-ui/core/colors/grey'; +import indigo from '@material-ui/core/colors/indigo'; +import orange from '@material-ui/core/colors/orange'; +import red from '@material-ui/core/colors/red'; +import teal from '@material-ui/core/colors/teal'; +import yellow from '@material-ui/core/colors/yellow'; + +export const primary = [ + blue[500], + red[500], + green[500], + yellow[500], + cyan[500], + orange[500], + teal[500], + indigo[500], + deepOrange[500], + deepPurple[500], +]; + +export const greyscale = [ + grey[300], + grey[400], + grey[200], + grey[500], + grey[100], + grey[600], +]; + +export const browns = [brown[500]]; diff --git a/plugins/opencost/src/constants/currencyCodes.js b/plugins/opencost/src/constants/currencyCodes.js new file mode 100644 index 0000000000..a325df54d0 --- /dev/null +++ b/plugins/opencost/src/constants/currencyCodes.js @@ -0,0 +1,196 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const currencyCodes = [ + 'AED', + 'AFN', + 'ALL', + 'AMD', + 'ANG', + 'AOA', + 'ARS', + 'AUD', + 'AWG', + 'AZN', + 'BAM', + 'BBD', + 'BDT', + 'BGN', + 'BHD', + 'BIF', + 'BMD', + 'BND', + 'BOB', + 'BOV', + 'BRL', + 'BSD', + 'BTN', + 'BWP', + 'BYR', + 'BZD', + 'CAD', + 'CDF', + 'CHE', + 'CHF', + 'CHW', + 'CLF', + 'CLP', + 'CNY', + 'COP', + 'COU', + 'CRC', + 'CUC', + 'CUP', + 'CVE', + 'CZK', + 'DJF', + 'DKK', + 'DOP', + 'DZD', + 'EGP', + 'ERN', + 'ETB', + 'EUR', + 'FJD', + 'FKP', + 'GBP', + 'GEL', + 'GHS', + 'GIP', + 'GMD', + 'GNF', + 'GTQ', + 'GYD', + 'HKD', + 'HNL', + 'HRK', + 'HTG', + 'HUF', + 'IDR', + 'ILS', + 'INR', + 'IQD', + 'IRR', + 'ISK', + 'JMD', + 'JOD', + 'JPY', + 'KES', + 'KGS', + 'KHR', + 'KMF', + 'KPW', + 'KRW', + 'KWD', + 'KYD', + 'KZT', + 'LAK', + 'LBP', + 'LKR', + 'LRD', + 'LSL', + 'LTL', + 'LVL', + 'LYD', + 'MAD', + 'MDL', + 'MGA', + 'MKD', + 'MMK', + 'MNT', + 'MOP', + 'MRO', + 'MUR', + 'MVR', + 'MWK', + 'MXN', + 'MXV', + 'MYR', + 'MZN', + 'NAD', + 'NGN', + 'NIO', + 'NOK', + 'NPR', + 'NZD', + 'OMR', + 'PAB', + 'PEN', + 'PGK', + 'PHP', + 'PKR', + 'PLN', + 'PYG', + 'QAR', + 'RON', + 'RSD', + 'RUB', + 'RWF', + 'SAR', + 'SBD', + 'SCR', + 'SDG', + 'SEK', + 'SGD', + 'SHP', + 'SLL', + 'SOS', + 'SRD', + 'SSP', + 'STD', + 'SYP', + 'SZL', + 'THB', + 'TJS', + 'TMT', + 'TND', + 'TOP', + 'TRY', + 'TTD', + 'TWD', + 'TZS', + 'UAH', + 'UGX', + 'USD', + 'USN', + 'USS', + 'UYI', + 'UYU', + 'UZS', + 'VEF', + 'VND', + 'VUV', + 'WST', + 'XAF', + 'XAG', + 'XAU', + 'XBA', + 'XBB', + 'XBC', + 'XBD', + 'XCD', + 'XDR', + 'XFU', + 'XOF', + 'XPD', + 'XPF', + 'XPT', + 'XTS', + 'XXX', + 'YER', + 'ZAR', + 'ZMW', +]; diff --git a/plugins/opencost/src/images/pig.png b/plugins/opencost/src/images/pig.png new file mode 100644 index 0000000000..5de5409e87 Binary files /dev/null and b/plugins/opencost/src/images/pig.png differ diff --git a/plugins/opencost/src/index.ts b/plugins/opencost/src/index.ts new file mode 100644 index 0000000000..30153e0c32 --- /dev/null +++ b/plugins/opencost/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * A Backstage plugin that integrates with OpenCost + * + * @packageDocumentation + */ + +export { openCostPlugin, OpenCostPage } from './plugin'; diff --git a/plugins/opencost/src/plugin.test.ts b/plugins/opencost/src/plugin.test.ts new file mode 100644 index 0000000000..6d6d20c603 --- /dev/null +++ b/plugins/opencost/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { openCostPlugin } from './plugin'; + +describe('opencost', () => { + it('should export plugin', () => { + expect(openCostPlugin).toBeDefined(); + }); +}); diff --git a/plugins/opencost/src/plugin.ts b/plugins/opencost/src/plugin.ts new file mode 100644 index 0000000000..2725cf3dde --- /dev/null +++ b/plugins/opencost/src/plugin.ts @@ -0,0 +1,40 @@ +/* + * 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 { + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +/** @public */ +export const openCostPlugin = createPlugin({ + id: 'opencost', + routes: { + root: rootRouteRef, + }, +}); + +/** @public */ +export const OpenCostPage = openCostPlugin.provide( + createRoutableExtension({ + name: 'OpenCostPage', + component: () => + import('./components/OpenCostPage').then(m => m.OpenCostPage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/opencost/src/routes.ts b/plugins/opencost/src/routes.ts new file mode 100644 index 0000000000..5f1f9e4527 --- /dev/null +++ b/plugins/opencost/src/routes.ts @@ -0,0 +1,21 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'opencost', +}); diff --git a/plugins/opencost/src/services/allocation.js b/plugins/opencost/src/services/allocation.js new file mode 100644 index 0000000000..e7fba8feae --- /dev/null +++ b/plugins/opencost/src/services/allocation.js @@ -0,0 +1,36 @@ +/* + * 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 axios from 'axios'; + +class AllocationService { + async fetchAllocation(baseUrl, win, aggregate, options) { + const { accumulate } = options; + const params = { + window: win, + aggregate: aggregate, + step: '1d', + }; + if (typeof accumulate === 'boolean') { + params.accumulate = accumulate; + } + + const result = await axios.get(`${baseUrl}/allocation/compute`, { params }); + + return result.data; + } +} + +export default new AllocationService(); diff --git a/plugins/opencost/src/util.js b/plugins/opencost/src/util.js new file mode 100644 index 0000000000..5854718a55 --- /dev/null +++ b/plugins/opencost/src/util.js @@ -0,0 +1,371 @@ +/* + * 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 { forEach, get, round } from 'lodash'; + +// rangeToCumulative takes an AllocationSetRange (type: array[AllocationSet]) +// and accumulates the values into a single AllocationSet (type: object) +export function rangeToCumulative(allocationSetRange, aggregateBy) { + if (allocationSetRange.length === 0) { + return null; + } + + const result = {}; + + forEach(allocationSetRange, allocSet => { + forEach(allocSet, alloc => { + if (result[alloc.name] === undefined) { + const hrs = get(alloc, 'minutes', 0) / 60.0; + + result[alloc.name] = { + name: alloc.name, + [aggregateBy]: alloc.name, + cpuCost: get(alloc, 'cpuCost', 0), + gpuCost: get(alloc, 'gpuCost', 0), + ramCost: get(alloc, 'ramCost', 0), + pvCost: get(alloc, 'pvCost', 0), + networkCost: get(alloc, 'networkCost', 0), + sharedCost: get(alloc, 'sharedCost', 0), + externalCost: get(alloc, 'externalCost', 0), + totalCost: get(alloc, 'totalCost', 0), + cpuUseCoreHrs: get(alloc, 'cpuCoreUsageAverage', 0) * hrs, + cpuReqCoreHrs: get(alloc, 'cpuCoreRequestAverage', 0) * hrs, + ramUseByteHrs: get(alloc, 'ramByteUsageAverage', 0) * hrs, + ramReqByteHrs: get(alloc, 'ramByteRequestAverage', 0) * hrs, + cpuEfficiency: get(alloc, 'cpuEfficiency', 0), + ramEfficiency: get(alloc, 'ramEfficiency', 0), + totalEfficiency: get(alloc, 'totalEfficiency', 0), + }; + } else { + const hrs = get(alloc, 'minutes', 0) / 60.0; + + result[alloc.name].cpuCost += get(alloc, 'cpuCost', 0); + result[alloc.name].gpuCost += get(alloc, 'gpuCost', 0); + result[alloc.name].ramCost += get(alloc, 'ramCost', 0); + result[alloc.name].pvCost += get(alloc, 'pvCost', 0); + result[alloc.name].networkCost += get(alloc, 'networkCost', 0); + result[alloc.name].sharedCost += get(alloc, 'sharedCost', 0); + result[alloc.name].externalCost += get(alloc, 'externalCost', 0); + result[alloc.name].totalCost += get(alloc, 'totalCost', 0); + result[alloc.name].cpuUseCoreHrs += + get(alloc, 'cpuCoreUsageAverage', 0) * hrs; + result[alloc.name].cpuReqCoreHrs += + get(alloc, 'cpuCoreRequestAverage', 0) * hrs; + result[alloc.name].ramUseByteHrs += + get(alloc, 'ramByteUsageAverage', 0) * hrs; + result[alloc.name].ramReqByteHrs += + get(alloc, 'ramByteRequestAverage', 0) * hrs; + } + }); + }); + + // If the range is of length > 1 (i.e. it is not just a single set) then + // compute efficiency for each result after accumulating. + if (allocationSetRange.length > 1) { + forEach(result, (alloc, name) => { + // If we can't compute total efficiency, it defaults to 0.0 + let totalEfficiency = 0.0; + + // CPU efficiency is defined as (usage/request). If request == 0.0 but + // usage > 0, then efficiency gets set to 1.0. + let cpuEfficiency = 0.0; + if (alloc.cpuReqCoreHrs > 0) { + cpuEfficiency = alloc.cpuUseCoreHrs / alloc.cpuReqCoreHrs; + } else if (alloc.cpuUseCoreHrs > 0) { + cpuEfficiency = 1.0; + } + + // RAM efficiency is defined as (usage/request). If request == 0.0 but + // usage > 0, then efficiency gets set to 1.0. + let ramEfficiency = 0.0; + if (alloc.ramReqByteHrs > 0) { + ramEfficiency = alloc.ramUseByteHrs / alloc.ramReqByteHrs; + } else if (alloc.ramUseByteHrs > 0) { + ramEfficiency = 1.0; + } + + // Compute efficiency as the cost-weighted average of CPU and RAM + // efficiency + if (alloc.cpuCost + alloc.ramCost > 0.0) { + totalEfficiency = + (alloc.cpuCost * cpuEfficiency + alloc.ramCost * ramEfficiency) / + (alloc.cpuCost + alloc.ramCost); + } + + result[name].cpuEfficiency = cpuEfficiency; + result[name].ramEfficiency = ramEfficiency; + result[name].totalEfficiency = totalEfficiency; + }); + } + + return result; +} + +// cumulativeToTotals adds each entry in the given AllocationSet (type: object) +// and returns a single Allocation (type: object) representing the totals +export function cumulativeToTotals(allocationSet) { + const totals = { + name: 'Totals', + cpuCost: 0, + gpuCost: 0, + ramCost: 0, + pvCost: 0, + networkCost: 0, + sharedCost: 0, + externalCost: 0, + totalCost: 0, + cpuEfficiency: 0, + ramEfficiency: 0, + totalEfficiency: 0, + }; + + // Use these for computing efficiency. As such, idle will not factor into + // these numbers, including CPU and RAM cost. + let cpuReqCoreHrs = 0; + let cpuUseCoreHrs = 0; + let ramReqByteHrs = 0; + let ramUseByteHrs = 0; + let cpuCost = 0; + let ramCost = 0; + + forEach(allocationSet, (alloc, name) => { + // Accumulate efficiency-related fields + if (name !== '__idle__') { + cpuReqCoreHrs += get(alloc, 'cpuReqCoreHrs', 0.0); + cpuUseCoreHrs += get(alloc, 'cpuUseCoreHrs', 0.0); + ramReqByteHrs += get(alloc, 'ramReqByteHrs', 0.0); + ramUseByteHrs += get(alloc, 'ramUseByteHrs', 0.0); + cpuCost += get(alloc, 'cpuCost', 0.0); + ramCost += get(alloc, 'ramCost', 0.0); + } + + // Sum cumulative fields + totals.cpuCost += get(alloc, 'cpuCost', 0); + totals.gpuCost += get(alloc, 'gpuCost', 0); + totals.ramCost += get(alloc, 'ramCost', 0); + totals.pvCost += get(alloc, 'pvCost', 0); + totals.networkCost += get(alloc, 'networkCost', 0); + totals.sharedCost += get(alloc, 'sharedCost', 0); + totals.externalCost += get(alloc, 'externalCost', 0); + totals.totalCost += get(alloc, 'totalCost', 0); + }); + + // Compute efficiency + if (cpuReqCoreHrs > 0) { + totals.cpuEfficiency = cpuUseCoreHrs / cpuReqCoreHrs; + } else if (cpuUseCoreHrs > 0) { + totals.cpuEfficiency = 1.0; + } + + if (ramReqByteHrs > 0) { + totals.ramEfficiency = ramUseByteHrs / ramReqByteHrs; + } else if (ramUseByteHrs > 0) { + totals.ramEfficiency = 1.0; + } + + if (cpuCost + ramCost > 0) { + totals.totalEfficiency = + (cpuCost * totals.cpuEfficiency + ramCost * totals.ramEfficiency) / + (cpuCost + ramCost); + } + + totals.cpuReqCoreHrs = cpuReqCoreHrs; + totals.cpuUseCoreHrs = cpuUseCoreHrs; + totals.ramReqByteHrs = ramReqByteHrs; + totals.ramUseByteHrs = ramUseByteHrs; + + return totals; +} + +export function toVerboseTimeRange(window) { + const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + + const start = new Date(); + start.setUTCHours(0, 0, 0, 0); + + const end = new Date(); + end.setUTCHours(0, 0, 0, 0); + + switch (window) { + case 'today': + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()}`; + case 'yesterday': + start.setUTCDate(start.getUTCDate() - 1); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()}`; + case 'week': + start.setUTCDate(start.getUTCDate() - start.getUTCDay()); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} until now`; + case 'month': + start.setUTCDate(1); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} until now`; + case 'lastweek': + start.setUTCDate(start.getUTCDate() - (start.getUTCDay() + 7)); + end.setUTCDate(end.getUTCDate() - (end.getUTCDay() + 1)); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${ + months[end.getUTCMonth()] + } ${end.getUTCFullYear()}`; + case 'lastmonth': + end.setUTCDate(1); + end.setUTCDate(end.getUTCDate() - 1); + start.setUTCDate(1); + start.setUTCDate(start.getUTCDate() - 1); + start.setUTCDate(1); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${ + months[end.getUTCMonth()] + } ${end.getUTCFullYear()}`; + case '6d': + start.setUTCDate(start.getUTCDate() - 6); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through now`; + case '29d': + start.setUTCDate(start.getUTCDate() - 29); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through now`; + case '59d': + start.setUTCDate(start.getUTCDate() - 59); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through now`; + case '89d': + start.setUTCDate(start.getUTCDate() - 89); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through now`; + // no default + } + + const splitDates = window.split(','); + if (checkCustomWindow(window) && splitDates.length > 1) { + const s = splitDates[0].split(/\D+/).slice(0, 3); + const e = splitDates[1].split(/\D+/).slice(0, 3); + if (s.length === 3 && e.length === 3) { + start.setUTCFullYear(s[0], s[1] - 1, s[2]); + end.setUTCFullYear(e[0], e[1] - 1, e[2]); + if (start === end) { + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()}`; + } + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${ + months[end.getUTCMonth()] + } ${end.getUTCFullYear()}`; + } + } + return ''; +} + +export function bytesToString(bytes) { + const ei = Math.pow(1024, 6); + if (bytes >= ei) { + return `${round(bytes / ei, 1)} EiB`; + } + const pi = Math.pow(1024, 5); + if (bytes >= pi) { + return `${round(bytes / pi, 1)} PiB`; + } + const ti = Math.pow(1024, 4); + if (bytes >= ti) { + return `${round(bytes / ti, 1)} TiB`; + } + const gi = Math.pow(1024, 3); + if (bytes >= gi) { + return `${round(bytes / gi, 1)} GiB`; + } + const mi = Math.pow(1024, 2); + if (bytes >= mi) { + return `${round(bytes / mi, 1)} MiB`; + } + const ki = Math.pow(1024, 1); + if (bytes >= ki) { + return `${round(bytes / ki, 1)} KiB`; + } + + return `${round(bytes, 1)} B`; +} + +const currencyLocale = 'en-US'; + +export function toCurrency(amount, currency, precision) { + if (typeof amount !== 'number') { + Console.warn( + `Tried to convert "${amount}" to currency, but it is not a number`, + ); + return ''; + } + + let ctype = currency; + + if (currency === undefined || currency === '') { + ctype = 'USD'; + } + + const opts = { + style: 'currency', + currency: ctype, + }; + + if (typeof precision === 'number') { + opts.minimumFractionDigits = precision; + opts.maximumFractionDigits = precision; + } + + return amount.toLocaleString(currencyLocale, opts); +} + +export function checkCustomWindow(window) { + // Example ISO interval string: 2020-12-02T00:00:00Z,2020-12-03T23:59:59Z + const customDateRegex = + /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z,\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/; + return customDateRegex.test(window); +} + +export default { + rangeToCumulative, + cumulativeToTotals, + toVerboseTimeRange, + bytesToString, + toCurrency, + checkCustomWindow, +}; diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index fc89f7aa33..3cda678f8a 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-org-react +## 0.1.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/org-react/api-report.md b/plugins/org-react/api-report.md index 48bd090f64..5cfe5ca90a 100644 --- a/plugins/org-react/api-report.md +++ b/plugins/org-react/api-report.md @@ -3,12 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { GroupEntity } from '@backstage/catalog-model'; +import { default as React_2 } from 'react'; // @public (undocumented) -export const GroupListPicker: (props: GroupListPickerProps) => JSX.Element; +export const GroupListPicker: ( + props: GroupListPickerProps, +) => React_2.JSX.Element; // @public export type GroupListPickerProps = { diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index ce49dd71d0..ba49738aa8 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.10", + "version": "0.1.12-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -40,6 +40,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 79dfedbd3d..349e7d055f 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-org +## 0.6.13-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.6.11 ### Patch Changes diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md index 7f21bfbb6b..704401e8d4 100644 --- a/plugins/org/api-report.md +++ b/plugins/org/api-report.md @@ -9,19 +9,21 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; // @public (undocumented) export const EntityGroupProfileCard: (props: { variant?: InfoCardVariants | undefined; showLinks?: boolean | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityMembersListCard: (props: { memberDisplayTitle?: string | undefined; pageSize?: number | undefined; showAggregateMembersToggle?: boolean | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityOwnershipCard: (props: { @@ -30,26 +32,26 @@ export const EntityOwnershipCard: (props: { hideRelationsToggle?: boolean | undefined; relationsType?: string | undefined; entityLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityUserProfileCard: (props: { variant?: InfoCardVariants | undefined; showLinks?: boolean | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const GroupProfileCard: (props: { variant?: InfoCardVariants; showLinks?: boolean; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const MembersListCard: (props: { memberDisplayTitle?: string; pageSize?: number; showAggregateMembersToggle?: boolean; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export const MyGroupsSidebarItem: (props: { @@ -57,7 +59,7 @@ export const MyGroupsSidebarItem: (props: { pluralTitle: string; icon: IconComponent; filter?: Record; -}) => JSX.Element | null; +}) => React_2.JSX.Element | null; // @public (undocumented) const orgPlugin: BackstagePlugin< @@ -77,11 +79,11 @@ export const OwnershipCard: (props: { hideRelationsToggle?: boolean; relationsType?: string; entityLimit?: number; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const UserProfileCard: (props: { variant?: InfoCardVariants; showLinks?: boolean; -}) => JSX.Element; +}) => React_2.JSX.Element; ``` diff --git a/plugins/org/package.json b/plugins/org/package.json index 48d53ca521..0193a1592b 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.6.11", + "version": "0.6.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "p-limit": "^3.1.0", "pluralize": "^8.0.0", "qs": "^6.10.1", @@ -61,7 +62,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index adc01d7b32..5ce6ebde10 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-pagerduty +## 0.6.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home-react@0.1.3-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.6.2 ### Patch Changes diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index f398a6bee7..24c2e103cf 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -12,12 +12,14 @@ import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; // @public (undocumented) export const EntityPagerDutyCard: ( props: EntityPagerDutyCardProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type EntityPagerDutyCardProps = { @@ -28,7 +30,7 @@ export type EntityPagerDutyCardProps = { // @public (undocumented) export const HomePagePagerDutyCard: ( props: CardExtensionProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type HomePagePagerDutyCardProps = PagerDutyEntity & { @@ -67,7 +69,7 @@ export type PagerDutyAssignee = { }; // @public @deprecated (undocumented) -export const PagerDutyCard: (props: EntityPagerDutyCardProps) => JSX.Element; +export const PagerDutyCard: (props: EntityPagerDutyCardProps) => JSX_2.Element; // @public @deprecated (undocumented) export type PagerDutyCardProps = EntityPagerDutyCardProps; @@ -215,7 +217,9 @@ export type PagerDutyUser = { }; // @public (undocumented) -export function TriggerButton(props: { children?: ReactNode }): JSX.Element; +export function TriggerButton(props: { + children?: ReactNode; +}): React_2.JSX.Element; // @public (undocumented) export class UnauthorizedError extends Error {} diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index e89f8715f4..e7787d5e78 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.6.2", + "version": "0.6.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/periskop-backend/CHANGELOG.md b/plugins/periskop-backend/CHANGELOG.md index 5f80406474..65613e8772 100644 --- a/plugins/periskop-backend/CHANGELOG.md +++ b/plugins/periskop-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-periskop-backend +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + ## 0.1.19 ### Patch Changes diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index bd828ed9b8..7d9f14486d 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop-backend", - "version": "0.1.19", + "version": "0.1.21-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/periskop/CHANGELOG.md b/plugins/periskop/CHANGELOG.md index 6bf38267f7..ef238aea2a 100644 --- a/plugins/periskop/CHANGELOG.md +++ b/plugins/periskop/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-periskop +## 0.1.21-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.19 ### Patch Changes diff --git a/plugins/periskop/api-report.md b/plugins/periskop/api-report.md index 3332dd9edb..578fb1dc49 100644 --- a/plugins/periskop/api-report.md +++ b/plugins/periskop/api-report.md @@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export interface AggregatedError { @@ -24,7 +25,7 @@ export interface AggregatedError { } // @public (undocumented) -export const EntityPeriskopErrorsCard: () => JSX.Element; +export const EntityPeriskopErrorsCard: () => JSX_2.Element; // @public (undocumented) interface Error_2 { diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index 7f044bc15f..2852092c64 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop", - "version": "0.1.19", + "version": "0.1.21-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -56,7 +57,6 @@ "@testing-library/user-event": "^14.0.0", "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 848a6ffb91..7904daa25e 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-permission-backend +## 0.5.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + ## 0.5.23 ### Patch Changes diff --git a/plugins/permission-backend/catalog-info.yaml b/plugins/permission-backend/catalog-info.yaml new file mode 100644 index 0000000000..13baa09802 --- /dev/null +++ b/plugins/permission-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-permission-backend + title: '@backstage/plugin-permission-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: permission-maintainers diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 0ab2eeee47..e9ff842ed2 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.23", + "version": "0.5.25-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/permission-common/catalog-info.yaml b/plugins/permission-common/catalog-info.yaml new file mode 100644 index 0000000000..d8d8918530 --- /dev/null +++ b/plugins/permission-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-permission-common + title: '@backstage/plugin-permission-common' + description: Isomorphic types and client for Backstage permissions and authorization +spec: + lifecycle: experimental + type: backstage-common-library + owner: permission-maintainers diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 7126954413..9039a4c036 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-permission-node +## 0.7.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + ## 0.7.11 ### Patch Changes diff --git a/plugins/permission-node/catalog-info.yaml b/plugins/permission-node/catalog-info.yaml new file mode 100644 index 0000000000..a85f952af8 --- /dev/null +++ b/plugins/permission-node/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-permission-node + title: '@backstage/plugin-permission-node' + description: Common permission and authorization utilities for backend plugins +spec: + lifecycle: experimental + type: backstage-node-library + owner: permission-maintainers diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index cfccd9678d..e97879c92b 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.7.11", + "version": "0.7.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 47aa6a70a3..4c7f21f588 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-permission-react +## 0.4.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-permission-common@0.7.7 + ## 0.4.14 ### Patch Changes diff --git a/plugins/permission-react/api-report.md b/plugins/permission-react/api-report.md index 293343b7ac..3c1961c92c 100644 --- a/plugins/permission-react/api-report.md +++ b/plugins/permission-react/api-report.md @@ -12,6 +12,7 @@ import { EvaluatePermissionRequest } from '@backstage/plugin-permission-common'; import { EvaluatePermissionResponse } from '@backstage/plugin-permission-common'; import { IdentityApi } from '@backstage/core-plugin-api'; import { Permission } from '@backstage/plugin-permission-common'; +import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { ResourcePermission } from '@backstage/plugin-permission-common'; @@ -65,7 +66,7 @@ export const PermissionedRoute: ( resourceRef: string | undefined; } ), -) => JSX.Element; +) => React_2.JSX.Element; // @public export function RequirePermission( diff --git a/plugins/permission-react/catalog-info.yaml b/plugins/permission-react/catalog-info.yaml new file mode 100644 index 0000000000..119f7320ab --- /dev/null +++ b/plugins/permission-react/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-permission-react + title: '@backstage/plugin-permission-react' +spec: + lifecycle: experimental + type: backstage-web-library + owner: permission-maintainers diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index a8308b5a75..5b90c79f16 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.4.14", + "version": "0.4.15-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/playlist-backend/CHANGELOG.md b/plugins/playlist-backend/CHANGELOG.md index 980d957538..efe61dab02 100644 --- a/plugins/playlist-backend/CHANGELOG.md +++ b/plugins/playlist-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-playlist-backend +## 0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-playlist-common@0.1.9 + ## 0.3.4 ### Patch Changes diff --git a/plugins/playlist-backend/package.json b/plugins/playlist-backend/package.json index 5a24b7cd6d..5b79ca8c33 100644 --- a/plugins/playlist-backend/package.json +++ b/plugins/playlist-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist-backend", - "version": "0.3.4", + "version": "0.3.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/playlist/CHANGELOG.md b/plugins/playlist/CHANGELOG.md index 09d4dd8560..4d1a36050d 100644 --- a/plugins/playlist/CHANGELOG.md +++ b/plugins/playlist/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-playlist +## 0.1.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-playlist-common@0.1.9 + - @backstage/plugin-search-react@1.6.5-next.0 + ## 0.1.13 ### Patch Changes diff --git a/plugins/playlist/api-report.md b/plugins/playlist/api-report.md index 7a732a4a53..cf0ebe8a37 100644 --- a/plugins/playlist/api-report.md +++ b/plugins/playlist/api-report.md @@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { Playlist } from '@backstage/plugin-playlist-common'; import { PlaylistMetadata } from '@backstage/plugin-playlist-common'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -17,7 +18,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const EntityPlaylistDialog: ( props: EntityPlaylistDialogProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type EntityPlaylistDialogProps = { @@ -93,7 +94,7 @@ export class PlaylistClient implements PlaylistApi { } // @public (undocumented) -export const PlaylistIndexPage: () => JSX.Element; +export const PlaylistIndexPage: () => JSX_2.Element; // @public (undocumented) export const playlistPlugin: BackstagePlugin< diff --git a/plugins/playlist/package.json b/plugins/playlist/package.json index 574aa712ab..6d45290430 100644 --- a/plugins/playlist/package.json +++ b/plugins/playlist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist", - "version": "0.1.13", + "version": "0.1.15-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,6 +42,7 @@ "@material-ui/core": "^4.12.2", "@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", "pluralize": "^8.0.0", "qs": "^6.9.4", @@ -64,7 +65,6 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0", "swr": "^2.0.0" }, diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index e9dd983ee4..d31aa942aa 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-proxy-backend +## 0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + ## 0.3.0 ### Minor Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index b710d4d47d..3a5e59ac3b 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-proxy-backend", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", - "version": "0.3.0", + "version": "0.3.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/puppetdb/CHANGELOG.md b/plugins/puppetdb/CHANGELOG.md index 7253466e95..e50ecfccd3 100644 --- a/plugins/puppetdb/CHANGELOG.md +++ b/plugins/puppetdb/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-puppetdb +## 0.1.6-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.1.4 ### Patch Changes diff --git a/plugins/puppetdb/api-report.md b/plugins/puppetdb/api-report.md index 48a3276460..c7d1d0ff0e 100644 --- a/plugins/puppetdb/api-report.md +++ b/plugins/puppetdb/api-report.md @@ -7,7 +7,9 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { PathParams } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; @@ -15,7 +17,7 @@ import { SubRouteRef } from '@backstage/core-plugin-api'; export const isPuppetDbAvailable: (entity: Entity) => boolean; // @public -export const PuppetDbPage: () => JSX.Element; +export const PuppetDbPage: () => JSX_2.Element; // @public const puppetdbPlugin: BackstagePlugin<{}, {}, {}>; @@ -29,7 +31,7 @@ export const puppetDbReportRouteRef: SubRouteRef>; export const puppetDbRouteRef: RouteRef; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/puppetdb/package.json b/plugins/puppetdb/package.json index 5973e91ee5..2eef5a880d 100644 --- a/plugins/puppetdb/package.json +++ b/plugins/puppetdb/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-puppetdb", "description": "Backstage plugin to visualize resource information and Puppet facts from PuppetDB.", - "version": "0.1.4", + "version": "0.1.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,6 +43,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.57", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -59,7 +60,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.1" }, "files": [ diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index c90db1c135..ae6e8a5892 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-rollbar-backend +## 0.1.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + ## 0.1.45 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index cdd3432a1f..db5630c39d 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar-backend", "description": "A Backstage backend plugin that integrates towards Rollbar", - "version": "0.1.45", + "version": "0.1.47-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index c8664e3c1e..e1641af7a6 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-rollbar +## 0.4.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + ## 0.4.21 ### Patch Changes diff --git a/plugins/rollbar/api-report.md b/plugins/rollbar/api-report.md index 5ee8a50ce3..45c672e7bb 100644 --- a/plugins/rollbar/api-report.md +++ b/plugins/rollbar/api-report.md @@ -10,13 +10,15 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EntityPageRollbar: () => JSX.Element; +export const EntityPageRollbar: () => React_2.JSX.Element; // @public (undocumented) -export const EntityRollbarContent: () => JSX.Element; +export const EntityRollbarContent: () => JSX_2.Element; // @public (undocumented) const isPluginApplicableToEntity: (entity: Entity) => boolean; @@ -238,5 +240,5 @@ export type RollbarTopActiveItem = { }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 7b2c5ce5bb..a53dd55f92 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.4.21", + "version": "0.4.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -41,6 +41,7 @@ "@material-ui/core": "^4.12.2", "@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-sparklines": "^1.7.0", "react-use": "^17.2.4" @@ -61,7 +62,6 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index 3d77b220e1..af00370e4b 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + ## 0.2.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index 1f93176163..1eb9c98ace 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", - "version": "0.2.1", + "version": "0.2.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 0c207a7d93..f3e101ddc8 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + ## 0.2.24 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 6283e20a78..01567b018b 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.2.24", + "version": "0.2.26-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index ba75fd998f..46153568b6 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + ## 0.2.3 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index e1f9a2fa2a..f4b934d1ee 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitlab", - "version": "0.2.3", + "version": "0.2.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 97cb1b9dec..424b6dbd60 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + ## 0.4.17 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index c6feb27fef..0547942371 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.4.17", + "version": "0.4.19-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index f252b34761..7fae11e623 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + ## 0.1.8 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 6114aa3088..d4d2e89f00 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-sentry", - "version": "0.1.8", + "version": "0.1.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index b188a8bdf0..ef767c8b1b 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + ## 0.2.21 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 9319c8001f..808f600fbd 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.2.21", + "version": "0.2.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 67827a6384..45dd6521cf 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-scaffolder-backend +## 1.16.3-next.0 + +### Patch Changes + +- a4989552d828: Add examples for `publish:github` and `publish:gitlab` scaffolder actions. +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- fb57a4694fc6: Fixed the plugin and module ID of the alpha `catalogModuleTemplateKind` export. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + ## 1.16.0 ### Minor Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 636074393f..4b3acd0331 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "1.16.0", + "version": "1.16.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts b/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts index 3359bd1dba..81d3dc3e93 100644 --- a/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts +++ b/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts @@ -24,8 +24,8 @@ import { ScaffolderEntitiesProcessor } from '../processor'; * @alpha */ export const catalogModuleTemplateKind = createBackendModule({ - moduleId: 'scaffolder', - pluginId: 'templateKind', + moduleId: 'templateKind', + pluginId: 'catalog', register(env) { env.registerInit({ deps: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index 9f68eb848a..9b7673ea29 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -42,7 +42,10 @@ describe('publish:azure', () => { const config = new ConfigReader({ integrations: { azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, { host: 'myazurehostnotoken.com' }, ], }, @@ -115,7 +118,9 @@ describe('publish:azure', () => { 'myazurehostnotoken.com?repo=bob&owner=owner&organization=org', }, }), - ).rejects.toThrow(/No token provided for Azure Integration/); + ).rejects.toThrow( + /No credentials provided https:\/\/myazurehostnotoken.com\/org, please check your integrations config/, + ); }); it('should throw when no repo is returned', async () => { @@ -260,7 +265,10 @@ describe('publish:azure', () => { const customAuthorConfig = new ConfigReader({ integrations: { azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, { host: 'myazurehostnotoken.com' }, ], }, @@ -301,7 +309,10 @@ describe('publish:azure', () => { const customAuthorConfig = new ConfigReader({ integrations: { azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, { host: 'myazurehostnotoken.com' }, ], }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index 96b7c87204..b6c0226163 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -15,10 +15,17 @@ */ import { InputError } from '@backstage/errors'; -import { ScmIntegrationRegistry } from '@backstage/integration'; +import { + DefaultAzureDevOpsCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; import { initRepoAndPush } from '../helpers'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; +import { + getBearerHandler, + getPersonalAccessTokenHandler, + WebApi, +} from 'azure-devops-node-api'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Config } from '@backstage/config'; @@ -135,22 +142,23 @@ export function createPublishAzureAction(options: { ); } - const integrationConfig = integrations.azure.byHost(host); + const url = `https://${host}/${organization}`; + const credentialProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(integrations); + const credentials = await credentialProvider.getCredentials({ url: url }); - if (!integrationConfig) { + if (credentials === undefined && ctx.input.token === undefined) { throw new InputError( - `No matching integration configuration for host ${host}, please check your integrations config`, + `No credentials provided ${url}, please check your integrations config`, ); } - if (!integrationConfig.config.token && !ctx.input.token) { - throw new InputError(`No token provided for Azure Integration ${host}`); - } + const authHandler = + ctx.input.token || credentials?.type === 'pat' + ? getPersonalAccessTokenHandler(ctx.input.token ?? credentials!.token) + : getBearerHandler(credentials!.token); - const token = ctx.input.token ?? integrationConfig.config.token!; - const authHandler = getPersonalAccessTokenHandler(token); - - const webApi = new WebApi(`https://${host}/${organization}`, authHandler); + const webApi = new WebApi(url, authHandler); const client = await webApi.getGitApi(); const createOptions: GitRepositoryCreateOptions = { name: repo }; const returnedRepo = await client.createRepository(createOptions, owner); @@ -187,14 +195,19 @@ export function createPublishAzureAction(options: { : config.getOptionalString('scaffolder.defaultAuthor.email'), }; + const auth = + ctx.input.token || credentials?.type === 'pat' + ? { + username: 'notempty', + password: ctx.input.token ?? credentials!.token, + } + : { token: credentials!.token }; + const commitResult = await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), remoteUrl, defaultBranch, - auth: { - username: 'notempty', - password: token, - }, + auth: auth, logger: ctx.logger, commitMessage: gitCommitMessage ? gitCommitMessage diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.test.ts new file mode 100644 index 0000000000..4d87ec97c4 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.test.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +jest.mock('../helpers'); + +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 { entityRefToName, initRepoAndPush } from '../helpers'; +import { createPublishGithubAction } from './github'; +import { examples } from './github.examples'; +import yaml from 'yaml'; + +const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU='; + +const initRepoAndPushMocked = initRepoAndPush as jest.Mock< + Promise<{ commitHash: string }> +>; + +const mockOctokit = { + rest: { + users: { + getByUsername: jest.fn(), + }, + repos: { + addCollaborator: jest.fn(), + createInOrg: jest.fn(), + createForAuthenticatedUser: jest.fn(), + replaceAllTopics: jest.fn(), + }, + teams: { + getByName: jest.fn(), + addOrUpdateRepoPermissionsInOrg: jest.fn(), + }, + actions: { + createRepoVariable: jest.fn(), + createOrUpdateRepoSecret: jest.fn(), + getRepoPublicKey: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('publish:github', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const { entityRefToName: realFamiliarizeEntityName } = + jest.requireActual('../helpers'); + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + 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({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createPublishGithubAction({ + integrations, + config, + githubCredentialsProvider, + }); + + // restore real implmentation + (entityRefToName as jest.Mock).mockImplementation( + realFamiliarizeEntityName, + ); + mockOctokit.rest.actions.getRepoPublicKey.mockResolvedValue({ + data: { + key: publicKey, + key_id: 'keyid', + }, + }); + }); + + afterEach(jest.resetAllMocks); + + it('should call initRepoAndPush with the correct values', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'master', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.ts new file mode 100644 index 0000000000..65f2ccdaa3 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.ts @@ -0,0 +1,70 @@ +/* + * 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: + 'Initializes a git repository of contents in workspace and publish it to GitHub with default configuration.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:github', + name: 'Publish to GitHub', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + }, + }, + ], + }), + }, + { + description: 'Add a description.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:github', + name: 'Publish to GitHub', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Initialize a git repository', + }, + }, + ], + }), + }, + { + description: 'Change visibility of the repository.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:github', + name: 'Publish to GitHub', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Initialize a git repository', + repoVisibility: 'public', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 9336c38b63..e5b6071863 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -30,6 +30,7 @@ import { import * as inputProps from '../github/inputProperties'; import * as outputProps from '../github/outputProperties'; import { parseRepoUrl } from './util'; +import { examples } from './github.examples'; /** * Creates a new action that initializes a git repository of the content in the workspace @@ -111,6 +112,7 @@ export function createPublishGithubAction(options: { id: 'publish:github', description: 'Initializes a git repository of contents in workspace and publishes it to GitHub.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.test.ts new file mode 100644 index 0000000000..5d58431c61 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import yaml from 'yaml'; + +jest.mock('../helpers', () => { + return { + initRepoAndPush: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + commitAndPushRepo: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + }; +}); + +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 '../helpers'; +import { examples } from './gitlab.examples'; + +const mockGitlabClient = { + Namespaces: { + show: jest.fn(), + }, + Projects: { + create: jest.fn(), + }, + Users: { + current: jest.fn(), + }, + ProjectMembers: { + add: jest.fn(), + }, +}; +jest.mock('@gitbeaker/node', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('publish:gitlab', () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishGitlabAction({ integrations, config }); + const mockContext = { + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should call initRepoAndPush with the correct values', async () => { + mockGitlabClient.Users.current.mockResolvedValue({ id: 12345 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Projects.create.mockResolvedValue({ + http_url_to_repo: 'http://mockurl.git', + }); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + defaultBranch: 'master', + remoteUrl: 'http://mockurl.git', + auth: { username: 'oauth2', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.ts new file mode 100644 index 0000000000..ac259866e3 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.ts @@ -0,0 +1,71 @@ +/* + * 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: + 'Initializes a git repository of the content in the workspace, and publishes it to GitLab.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitlab', + name: 'Publish to GitLab', + input: { + repoUrl: 'gitlab.com?repo=project_name&owner=group_name', + }, + }, + ], + }), + }, + { + description: 'Add a description.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitlab', + name: 'Publish to GitLab', + input: { + repoUrl: 'gitlab.com?repo=project_name&owner=group_name', + description: 'Initialize a git repository', + }, + }, + ], + }), + }, + { + description: + 'Sets the commit message on the repository. The default value is `initial commit`.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitlab', + name: 'Publish to GitLab', + input: { + repoUrl: 'gitlab.com?repo=project_name&owner=group_name', + description: 'Initialize a git repository', + gitCommitMessage: 'Started a project.', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 4045fd6958..dbab7b11b8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -21,6 +21,7 @@ import { Gitlab } from '@gitbeaker/node'; import { initRepoAndPush } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; +import { examples } from './gitlab.examples'; /** * Creates a new action that initializes a git repository of the content in the workspace @@ -49,6 +50,7 @@ export function createPublishGitlabAction(options: { id: 'publish:gitlab', description: 'Initializes a git repository of the content in the workspace, and publishes it to GitLab.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index f3cb9b645e..c96bd6b9cd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -261,18 +261,21 @@ export class DatabaseTaskStore implements TaskStore { tasks: { taskId: string }[]; }> { const { timeoutS } = options; - + let heartbeatInterval = this.db.raw(`? - interval '${timeoutS} seconds'`, [ + this.db.fn.now(), + ]); + if (this.db.client.config.client.includes('mysql')) { + heartbeatInterval = this.db.raw( + `date_sub(now(), interval ${timeoutS} second)`, + ); + } else if (this.db.client.config.client.includes('sqlite3')) { + heartbeatInterval = this.db.raw(`datetime('now', ?)`, [ + `-${timeoutS} seconds`, + ]); + } const rawRows = await this.db('tasks') .where('status', 'processing') - .andWhere( - 'last_heartbeat_at', - '<=', - this.db.client.config.client.includes('sqlite3') - ? this.db.raw(`datetime('now', ?)`, [`-${timeoutS} seconds`]) - : this.db.raw(`? - interval '${timeoutS} seconds'`, [ - this.db.fn.now(), - ]), - ); + .andWhere('last_heartbeat_at', '<=', heartbeatInterval); const tasks = rawRows.map(row => ({ taskId: row.id, })); diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index ad964812f7..05374afbff 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-node +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-common@1.4.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index ac22b7c2d6..0f0a3d51ed 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-node", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", - "version": "0.2.0", + "version": "0.2.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 91cde8ccf2..19ab0110b6 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-scaffolder-react +## 1.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + ## 1.5.2 ### Patch Changes diff --git a/plugins/scaffolder-react/alpha-api-report.md b/plugins/scaffolder-react/alpha-api-report.md index 58a3c8e208..11bb2c10c4 100644 --- a/plugins/scaffolder-react/alpha-api-report.md +++ b/plugins/scaffolder-react/alpha-api-report.md @@ -44,10 +44,10 @@ export function createNextScaffolderFieldExtension< // @alpha export const DefaultTemplateOutputs: (props: { output?: ScaffolderTaskOutput; -}) => JSX.Element | null; +}) => React_2.JSX.Element | null; // @alpha (undocumented) -export const EmbeddableWorkflow: (props: WorkflowProps) => JSX.Element; +export const EmbeddableWorkflow: (props: WorkflowProps) => React_2.JSX.Element; // @alpha export const extractSchemaFromStep: (inputStep: JsonObject) => { @@ -56,7 +56,9 @@ export const extractSchemaFromStep: (inputStep: JsonObject) => { }; // @alpha -export const Form: (props: PropsWithChildren) => JSX.Element; +export const Form: ( + props: PropsWithChildren, +) => React_2.JSX.Element; // @alpha export type FormProps = Pick< @@ -123,7 +125,7 @@ export interface ParsedTemplateSchema { } // @alpha -export const ReviewState: (props: ReviewStateProps) => JSX.Element; +export const ReviewState: (props: ReviewStateProps) => React_2.JSX.Element; // @alpha export type ReviewStateProps = { @@ -134,7 +136,7 @@ export type ReviewStateProps = { // @alpha export const ScaffolderField: ( props: PropsWithChildren, -) => JSX.Element; +) => React_2.JSX.Element; // @alpha export interface ScaffolderFieldProps { @@ -159,7 +161,7 @@ export interface ScaffolderFieldProps { // @alpha (undocumented) export function ScaffolderPageContextMenu( props: ScaffolderPageContextMenuProps, -): JSX.Element | null; +): React_2.JSX.Element | null; // @alpha (undocumented) export type ScaffolderPageContextMenuProps = { @@ -169,7 +171,7 @@ export type ScaffolderPageContextMenuProps = { }; // @alpha -export const Stepper: (stepperProps: StepperProps) => JSX.Element; +export const Stepper: (stepperProps: StepperProps) => React_2.JSX.Element; // @alpha export type StepperProps = { @@ -192,10 +194,10 @@ export const TaskLogStream: (props: { logs: { [k: string]: string[]; }; -}) => JSX.Element; +}) => React_2.JSX.Element; // @alpha -export const TaskSteps: (props: TaskStepsProps) => JSX.Element; +export const TaskSteps: (props: TaskStepsProps) => React_2.JSX.Element; // @alpha export interface TaskStepsProps { @@ -210,7 +212,7 @@ export interface TaskStepsProps { } // @alpha -export const TemplateCard: (props: TemplateCardProps) => JSX.Element; +export const TemplateCard: (props: TemplateCardProps) => React_2.JSX.Element; // @alpha export interface TemplateCardProps { @@ -227,10 +229,12 @@ export interface TemplateCardProps { } // @alpha -export const TemplateCategoryPicker: () => JSX.Element | null; +export const TemplateCategoryPicker: () => React_2.JSX.Element | null; // @alpha -export const TemplateGroup: (props: TemplateGroupProps) => JSX.Element | null; +export const TemplateGroup: ( + props: TemplateGroupProps, +) => React_2.JSX.Element | null; // @alpha (undocumented) export type TemplateGroupFilter = { @@ -260,7 +264,9 @@ export interface TemplateGroupProps { } // @alpha (undocumented) -export const TemplateGroups: (props: TemplateGroupsProps) => JSX.Element | null; +export const TemplateGroups: ( + props: TemplateGroupsProps, +) => React_2.JSX.Element | null; // @alpha (undocumented) export interface TemplateGroupsProps { diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index c618050810..23204695a4 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -295,7 +295,7 @@ export interface ScaffolderUseTemplateSecrets { // @public export const SecretsContextProvider: ( props: PropsWithChildren<{}>, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type TaskStream = { diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 657a66e6e3..5f77af8472 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-react", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", - "version": "1.5.2", + "version": "1.5.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 9098706875..1a33f547ad 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-scaffolder +## 1.14.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/plugin-scaffolder-react@1.5.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + ## 1.14.2 ### Patch Changes diff --git a/plugins/scaffolder/alpha-api-report.md b/plugins/scaffolder/alpha-api-report.md index de657c2138..8cacf6105e 100644 --- a/plugins/scaffolder/alpha-api-report.md +++ b/plugins/scaffolder/alpha-api-report.md @@ -7,6 +7,7 @@ import { FormProps as FormProps_2 } from '@backstage/plugin-scaffolder-react/alpha'; import type { FormProps as FormProps_3 } from '@rjsf/core-v5'; +import { JSX as JSX_2 } from 'react'; import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; import { PropsWithChildren } from 'react'; @@ -47,7 +48,7 @@ export type NextRouterProps = { // @alpha export const NextScaffolderPage: ( props: PropsWithChildren, -) => JSX.Element; +) => JSX_2.Element; // @alpha (undocumented) export type TemplateListPageProps = { diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 48f24868de..59d2d54fcc 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -23,6 +23,7 @@ import { FieldExtensionOptions as FieldExtensionOptions_2 } from '@backstage/plu import { FieldValidation } from '@rjsf/core'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; +import { JSX as JSX_2 } from 'react'; import { LayoutOptions as LayoutOptions_2 } from '@backstage/plugin-scaffolder-react'; import { LayoutTemplate as LayoutTemplate_2 } from '@backstage/plugin-scaffolder-react'; import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin-scaffolder-react'; @@ -500,7 +501,7 @@ export const ScaffolderLayouts: ComponentType<{ export type ScaffolderOutputlink = ScaffolderOutputLink; // @public -export const ScaffolderPage: (props: RouterProps) => JSX.Element; +export const ScaffolderPage: (props: RouterProps) => JSX_2.Element; // @public export const scaffolderPlugin: BackstagePlugin< @@ -550,7 +551,7 @@ export type ScaffolderTaskStatus = ScaffolderTaskStatus_2; export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecrets_2; // @public -export const TaskPage: (props: TaskPageProps) => JSX.Element; +export const TaskPage: (props: TaskPageProps) => React_2.JSX.Element; // @public export type TaskPageProps = { @@ -561,7 +562,7 @@ export type TaskPageProps = { export type TemplateParameterSchema = TemplateParameterSchema_2; // @public -export const TemplateTypePicker: () => JSX.Element | null; +export const TemplateTypePicker: () => React_2.JSX.Element | null; // @public @deprecated (undocumented) export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets_2; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index f4dae39d96..b67ebd35be 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "1.14.2", + "version": "1.14.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index ce170b95dd..639d1ccb44 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend-module-catalog +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + ## 0.1.4 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/catalog-info.yaml b/plugins/search-backend-module-catalog/catalog-info.yaml new file mode 100644 index 0000000000..d36627848e --- /dev/null +++ b/plugins/search-backend-module-catalog/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-catalog + title: '@backstage/plugin-search-backend-module-catalog' + description: A module for the search backend that exports catalog modules +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index e3509aec8a..84e03dac3f 100644 --- a/plugins/search-backend-module-catalog/package.json +++ b/plugins/search-backend-module-catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-catalog", "description": "A module for the search backend that exports catalog modules", - "version": "0.1.4", + "version": "0.1.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index 0d50bb342d..f17f9852fb 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/integration-aws-node@0.1.5 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + ## 1.3.3 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/catalog-info.yaml b/plugins/search-backend-module-elasticsearch/catalog-info.yaml new file mode 100644 index 0000000000..300d1034c9 --- /dev/null +++ b/plugins/search-backend-module-elasticsearch/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-elasticsearch + title: '@backstage/plugin-search-backend-module-elasticsearch' + description: A module for the search backend that implements search using ElasticSearch +spec: + lifecycle: production + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 63e6868ad6..6d31791dbd 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "1.3.3", + "version": "1.3.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-explore/CHANGELOG.md b/plugins/search-backend-module-explore/CHANGELOG.md index 05c17f7c59..8942db053d 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-backend-module-explore +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + ## 0.1.4 ### Patch Changes diff --git a/plugins/search-backend-module-explore/catalog-info.yaml b/plugins/search-backend-module-explore/catalog-info.yaml new file mode 100644 index 0000000000..237e7a0f98 --- /dev/null +++ b/plugins/search-backend-module-explore/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-explore + title: '@backstage/plugin-search-backend-module-explore' + description: A module for the search backend that exports explore modules +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index 8e4781b081..a867bf04e1 100644 --- a/plugins/search-backend-module-explore/package.json +++ b/plugins/search-backend-module-explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-explore", "description": "A module for the search backend that exports explore modules", - "version": "0.1.4", + "version": "0.1.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index d85c7f3e91..fe2f4ef98e 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + ## 0.5.9 ### Patch Changes diff --git a/plugins/search-backend-module-pg/catalog-info.yaml b/plugins/search-backend-module-pg/catalog-info.yaml new file mode 100644 index 0000000000..47b9717882 --- /dev/null +++ b/plugins/search-backend-module-pg/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-pg + title: '@backstage/plugin-search-backend-module-pg' + description: A module for the search backend that implements search using PostgreSQL +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index de77f80e66..67b859fe2d 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-pg", "description": "A module for the search backend that implements search using PostgreSQL", - "version": "0.5.9", + "version": "0.5.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 1434db16c4..1043f5155d 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + ## 0.1.4 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/catalog-info.yaml b/plugins/search-backend-module-techdocs/catalog-info.yaml new file mode 100644 index 0000000000..a0bbfc5bb7 --- /dev/null +++ b/plugins/search-backend-module-techdocs/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-techdocs + title: '@backstage/plugin-search-backend-module-techdocs' + description: A module for the search backend that exports techdocs modules +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index 51e59626f3..9fad327264 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-techdocs", "description": "A module for the search backend that exports techdocs modules", - "version": "0.1.4", + "version": "0.1.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 9d82992c80..f4acfe2a44 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-backend-node +## 1.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + ## 1.2.4 ### Patch Changes diff --git a/plugins/search-backend-node/catalog-info.yaml b/plugins/search-backend-node/catalog-info.yaml new file mode 100644 index 0000000000..5f6b36256f --- /dev/null +++ b/plugins/search-backend-node/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-node + title: '@backstage/plugin-search-backend-node' + description: >- + A library for Backstage backend plugins that want to interact with the + search backend plugin +spec: + lifecycle: production + type: backstage-node-library + owner: discoverability-maintainers diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 19faf1f0b4..a35ae07519 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "1.2.4", + "version": "1.2.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 8538ffc08f..2e51dfc137 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-search-backend +## 1.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + ## 1.4.0 ### Minor Changes diff --git a/plugins/search-backend/catalog-info.yaml b/plugins/search-backend/catalog-info.yaml new file mode 100644 index 0000000000..2b8c7ee346 --- /dev/null +++ b/plugins/search-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend + title: '@backstage/plugin-search-backend' + description: The Backstage backend plugin that provides your backstage app with search +spec: + lifecycle: production + type: backstage-backend-plugin + owner: discoverability-maintainers diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index ba36bea2a9..abf68589ea 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "1.4.0", + "version": "1.4.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-common/catalog-info.yaml b/plugins/search-common/catalog-info.yaml new file mode 100644 index 0000000000..d7e77a8d10 --- /dev/null +++ b/plugins/search-common/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-common + title: '@backstage/plugin-search-common' + description: >- + Common functionalities for Search, to be shared between various + search-enabled plugins +spec: + lifecycle: production + type: backstage-common-library + owner: discoverability-maintainers diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index e926722cbb..733bee3e88 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-react +## 1.6.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.5 + ## 1.6.4 ### Patch Changes diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 8afde8c4a5..97c8300c14 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -31,10 +31,12 @@ import { TypographyProps } from '@material-ui/core'; // @public (undocumented) export const AutocompleteFilter: ( props: SearchAutocompleteFilterProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) -export const CheckboxFilter: (props: SearchFilterComponentProps) => JSX.Element; +export const CheckboxFilter: ( + props: SearchFilterComponentProps, +) => React_2.JSX.Element; // @public export const createSearchResultListItemExtension: < @@ -46,7 +48,7 @@ export const createSearchResultListItemExtension: < // @public (undocumented) export const DefaultResultListItem: ( props: DefaultResultListItemProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type DefaultResultListItemProps = { @@ -62,7 +64,7 @@ export type DefaultResultListItemProps = { // @public (undocumented) export const HighlightedSearchResultText: ( props: HighlightedSearchResultTextProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type HighlightedSearchResultTextProps = { @@ -100,7 +102,7 @@ export type SearchAutocompleteComponent =