diff --git a/.changeset/brown-cycles-beg.md b/.changeset/brown-cycles-beg.md new file mode 100644 index 0000000000..98df27a00f --- /dev/null +++ b/.changeset/brown-cycles-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Hide text output button if only one is present diff --git a/.changeset/fast-rabbits-unite.md b/.changeset/fast-rabbits-unite.md new file mode 100644 index 0000000000..f4686b32cc --- /dev/null +++ b/.changeset/fast-rabbits-unite.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +**DEPRECATION**: The `getOctokitOptions` function signature with `repoUrl` option has been deprecated in favour of a function signature with individual `host`, `owner`, and `repo` parameters: + +```diff + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider, + token, +- repoUrl, ++ host, ++ owner, ++ repo, + }); +``` diff --git a/.changeset/fluffy-carrots-wink.md b/.changeset/fluffy-carrots-wink.md new file mode 100644 index 0000000000..a6f09d9558 --- /dev/null +++ b/.changeset/fluffy-carrots-wink.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING ALPHA**: Removed the deprecated `featureDiscoveryServiceRef` and `FeatureDiscoveryService`. diff --git a/.changeset/happy-windows-shop.md b/.changeset/happy-windows-shop.md new file mode 100644 index 0000000000..2c8d32a0dc --- /dev/null +++ b/.changeset/happy-windows-shop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +feat: add cancel button for WidgetSettingsOverlay diff --git a/.changeset/heavy-olives-thank.md b/.changeset/heavy-olives-thank.md new file mode 100644 index 0000000000..40ca9e16ed --- /dev/null +++ b/.changeset/heavy-olives-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +`createGitlabProjectMigrateAction` can now output the `migrationId` diff --git a/.changeset/hot-mice-cry.md b/.changeset/hot-mice-cry.md new file mode 100644 index 0000000000..e35538d2de --- /dev/null +++ b/.changeset/hot-mice-cry.md @@ -0,0 +1,6 @@ +--- +'@backstage/cli-node': patch +'@backstage/cli': patch +--- + +Fixed an issue where default feature type information wasn't being added to package.json/exports before publishing if exports didn't exist beforehand diff --git a/.changeset/hot-planes-hear.md b/.changeset/hot-planes-hear.md new file mode 100644 index 0000000000..6ef29772ef --- /dev/null +++ b/.changeset/hot-planes-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Internal refactor to stop importing the removed `FeatureDiscoveryService` from `@backstage/backend-plugin-api`. diff --git a/.changeset/large-pans-brake.md b/.changeset/large-pans-brake.md new file mode 100644 index 0000000000..619e697f46 --- /dev/null +++ b/.changeset/large-pans-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Upgrade `dagre` to `@dagrejs/dagre` diff --git a/.changeset/many-goats-flash.md b/.changeset/many-goats-flash.md new file mode 100644 index 0000000000..f3fec7e79a --- /dev/null +++ b/.changeset/many-goats-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +**BREAKING ALPHA**: Removed the deprecated `featureDiscoveryServiceFactory`. Existing usage can be replaced with `discoveryFeatureLoader` from `@backstage/backend-defaults`. diff --git a/.changeset/pretty-worms-walk.md b/.changeset/pretty-worms-walk.md new file mode 100644 index 0000000000..890b0dca5b --- /dev/null +++ b/.changeset/pretty-worms-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Allow passing IP type to use with cloud-sql-connector diff --git a/.changeset/real-foxes-film.md b/.changeset/real-foxes-film.md new file mode 100644 index 0000000000..22f1ecead2 --- /dev/null +++ b/.changeset/real-foxes-film.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-cluster': patch +'@backstage/plugin-kubernetes-common': patch +'@backstage/plugin-kubernetes': patch +--- + +The `/clusters` endpoint is now protected by the `kubernetes.clusters.read` permission. +The `/services/:serviceId` endpoint is now protected by the `kubernetes.resources.read` permission. +The `/resources` endpoints are now protected by the `kubernetes.resources.read` permission. diff --git a/.changeset/serious-ads-leave.md b/.changeset/serious-ads-leave.md new file mode 100644 index 0000000000..9caf1bc461 --- /dev/null +++ b/.changeset/serious-ads-leave.md @@ -0,0 +1,22 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': minor +'@backstage/plugin-auth-backend-module-bitbucket-server-provider': minor +'@backstage/plugin-auth-backend-module-vmware-cloud-provider': minor +'@backstage/plugin-auth-backend-module-atlassian-provider': minor +'@backstage/plugin-auth-backend-module-bitbucket-provider': minor +'@backstage/plugin-auth-backend-module-microsoft-provider': minor +'@backstage/plugin-auth-backend-module-onelogin-provider': minor +'@backstage/plugin-auth-backend-module-pinniped-provider': minor +'@backstage/plugin-auth-backend-module-aws-alb-provider': minor +'@backstage/plugin-auth-backend-module-gcp-iap-provider': minor +'@backstage/plugin-auth-backend-module-github-provider': minor +'@backstage/plugin-auth-backend-module-gitlab-provider': minor +'@backstage/plugin-auth-backend-module-google-provider': minor +'@backstage/plugin-auth-backend-module-oauth2-provider': minor +'@backstage/plugin-auth-backend-module-auth0-provider': minor +'@backstage/plugin-auth-backend-module-oidc-provider': minor +'@backstage/plugin-auth-backend-module-okta-provider': minor +'@backstage/plugin-auth-node': minor +--- + +Added `auth.providers..sessionDuration` config for auth providers to allow the lifespan of user sessions to be configured. diff --git a/.changeset/smart-ligers-sniff.md b/.changeset/smart-ligers-sniff.md new file mode 100644 index 0000000000..42353eca2b --- /dev/null +++ b/.changeset/smart-ligers-sniff.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/plugin-scaffolder': patch +--- + +Fixed bug of passing wrong value to `onChange` handler when using `GitLab` autocomplete diff --git a/.changeset/tidy-oranges-allow.md b/.changeset/tidy-oranges-allow.md new file mode 100644 index 0000000000..78d5393cec --- /dev/null +++ b/.changeset/tidy-oranges-allow.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Added experimental flag for scaffolder to wait for running tasks to complete on shutdown + +Enabling the `EXPERIMENTAL_gracefulShutdown` flag in the scaffolder config will make the +scaffolder block the shutdown process until all running tasks have completed. This is useful +when there is a need to ensure that all tasks have completed before the scaffolder is shut down. + +Please note, that the `TaskWorker` `stop` method is now asynchronous and awaited for the +tasks to complete when the experimental flag is enabled. diff --git a/.changeset/young-plants-report.md b/.changeset/young-plants-report.md new file mode 100644 index 0000000000..b35fb4b472 --- /dev/null +++ b/.changeset/young-plants-report.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dynamic-feature-service': minor +--- + +**BREAKING**: removed the deprecated `dynamicPluginsFeatureDiscoveryServiceFactory`. diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index 36e31565b9..34d34ef5a6 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -3,6 +3,8 @@ on: push: branches: - master + paths: + - '.github/workflows/sync_version-packages.yml' concurrency: group: sync-version-packages @@ -23,6 +25,10 @@ jobs: fetch-depth: 20000 fetch-tags: true token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + + - name: Fetch tags # See https://github.com/actions/checkout/issues/2041 + run: git fetch --tags + - name: Install Dependencies run: yarn --immutable - name: Create Release Pull Request diff --git a/docs/auth/atlassian/provider.md b/docs/auth/atlassian/provider.md index a1009b586e..ffee3454a9 100644 --- a/docs/auth/atlassian/provider.md +++ b/docs/auth/atlassian/provider.md @@ -51,6 +51,8 @@ auth: additionalScopes: - 'read:jira-user' - 'read:jira-work' + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/atlassian/provider#resolvers for more resolvers @@ -67,6 +69,10 @@ The Atlassian provider is a structure with the following configuration keys: **NOTE:** The scopes `offline_access`, `read:jira-work`, and `read:jira-user` are provided by default. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/auth0/provider.md b/docs/auth/auth0/provider.md index 2873a2cace..44a5bb0e43 100644 --- a/docs/auth/auth0/provider.md +++ b/docs/auth/auth0/provider.md @@ -44,6 +44,8 @@ auth: audience: ${AUTH_AUTH0_AUDIENCE} connection: ${AUTH_AUTH0_CONNECTION} connectionScope: ${AUTH_AUTH0_CONNECTION_SCOPE} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code session: secret: ${AUTH_SESSION_SECRET} ``` @@ -66,6 +68,7 @@ Auth0 requires a session, so you need to give the session a secret key. - `audience`: The intended recipients of the token. - `connection`: Social identity provider name. To check the available social connections, please visit [Auth0 Social Connections](https://marketplace.auth0.com/features/social-connections). - `connectionScope`: Additional scopes in the interactive token request. It should always be used in combination with the `connection` parameter. +- `sessionDuration`: Lifespan of the user session. ### Resolvers diff --git a/docs/auth/aws-alb/provider.md b/docs/auth/aws-alb/provider.md index 5380877f86..5f289170d2 100644 --- a/docs/auth/aws-alb/provider.md +++ b/docs/auth/aws-alb/provider.md @@ -23,6 +23,8 @@ auth: signer: 'arn:aws:elasticloadbalancing:us-east-2:123456789012:loadbalancer/app/my-load-balancer/1234567890123456' # this is the region where your ALB instance resides region: 'us-west-2' + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/aws-alb/provider#resolvers for more resolvers @@ -31,6 +33,10 @@ auth: Ensure that you have set the signer correctly. It is also recommended that you restrict your target groups' security policy to only accept connections from that ALB. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index e84e58c70a..f8a1fb2ab2 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -37,6 +37,8 @@ auth: development: clientId: ${AUTH_BITBUCKET_CLIENT_ID} clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/bitbucket/provider#resolvers for more resolvers @@ -49,6 +51,10 @@ The Bitbucket provider is a structure with two configuration keys: `b59241722e3c3b4816e2` - `clientSecret`: The Secret tied to the generated Key. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/bitbucketServer/provider.md b/docs/auth/bitbucketServer/provider.md index 6063796dd2..0da600d3fd 100644 --- a/docs/auth/bitbucketServer/provider.md +++ b/docs/auth/bitbucketServer/provider.md @@ -34,6 +34,8 @@ auth: host: bitbucket.example.org clientId: ${AUTH_BITBUCKET_SERVER_CLIENT_ID} clientSecret: ${AUTH_BITBUCKET_SERVER_CLIENT_SECRET} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code ``` The Bitbucket Server provider is a structure with two configuration keys: @@ -41,6 +43,10 @@ The Bitbucket Server provider is a structure with two configuration keys: - `clientId`: The client ID that was generated by Bitbucket, e.g. `b0f868455c15dcdff5c5fb5d173ae684`. - `clientSecret`: The client secret tied to the generated client ID. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/cloudflare/provider.md b/docs/auth/cloudflare/provider.md index 7611a0ca48..e8d72dc3ba 100644 --- a/docs/auth/cloudflare/provider.md +++ b/docs/auth/cloudflare/provider.md @@ -38,6 +38,8 @@ auth: # You can customize the authorization cookie name, by default # CF_Authorization is used authorizationCookieName: + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code # This picks what sign in resolver(s) you want to use. signIn: resolvers: @@ -47,6 +49,10 @@ auth: This config section must be in place for the provider to load at all. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/github/provider.md b/docs/auth/github/provider.md index 51610b36ec..fa44ae9405 100644 --- a/docs/auth/github/provider.md +++ b/docs/auth/github/provider.md @@ -49,6 +49,8 @@ auth: clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} ## uncomment if using GitHub Enterprise # enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/github/provider#resolvers for more resolvers @@ -66,6 +68,7 @@ The GitHub provider is a structure with these configuration keys: initiating an OAuth flow, e.g., `https://your-intermediate-service.com/handler`. Only needed if Backstage is not the immediate receiver (e.g., one OAuth app for many backstage instances). +- `sessionDuration` (optional): Lifespan of the user session. - `signIn`: The configuration for the sign-in process, including the **resolvers** that should be used to match the user from the auth provider with the user entity in the Backstage catalog (typically a single resolver is sufficient). diff --git a/docs/auth/gitlab/provider.md b/docs/auth/gitlab/provider.md index 28f204d785..a76c03d10f 100644 --- a/docs/auth/gitlab/provider.md +++ b/docs/auth/gitlab/provider.md @@ -45,6 +45,8 @@ auth: # audience: https://gitlab.company.com ## uncomment if using a custom redirect URI # callbackUrl: https://${BASE_URL}/api/auth/gitlab/handler/frame + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/gitlab/provider#resolvers for more resolvers @@ -61,6 +63,7 @@ The GitLab provider is a structure with three configuration keys: - `callbackUrl` (optional): The URL matching the Redirect URI registered when creating your GitLab OAuth App, e.g. `https://$backstage.acme.corp/api/auth/gitlab/handler/frame` Note: Due to a peculiarity with GitLab OAuth, ensure there is no trailing `/` after 'frame' in the URL. +- `sessionDuration` (optional): Lifespan of the user session. ### Resolvers diff --git a/docs/auth/google/provider.md b/docs/auth/google/provider.md index e03985d432..9a550d5ee0 100644 --- a/docs/auth/google/provider.md +++ b/docs/auth/google/provider.md @@ -42,6 +42,8 @@ auth: development: clientId: ${AUTH_GOOGLE_CLIENT_ID} clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/google/provider#resolvers for more resolvers @@ -54,6 +56,10 @@ The Google provider is a structure with two configuration keys: `10023341500512-beui241gjwwkrdkr2eh7dprewj2pp1q.apps.googleusercontent.com` - `clientSecret`: The client secret tied to the generated client ID. +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index 74416f853e..39eed92cba 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -84,6 +84,7 @@ The Microsoft provider is a structure with three mandatory configuration keys: For more details, see [Home Realm Discovery](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/home-realm-discovery-policy) - `additionalScopes` (optional): List of scopes for the App Registration, to be requested in addition to the required ones. - `skipUserProfile` (optional): If true, skips loading the user profile even if the `User.Read` scope is present. This is a performance optimization during login and can be used with resolvers that only needs the email address in `spec.profile.email` obtained when the `email` OAuth2 scope is present. +- `sessionDuration` (optional): Lifespan of the user session. ### Resolvers diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index 761d049997..838ac51f1f 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -263,6 +263,7 @@ check the App Registration you created: basically the same thing. - `prompt`: Recommended to use `auto` so the browser will request login to the IDP if the user has no active session. +- `sessionDuration` (optional): Lifespan of the user session. Note that for the time being, any change in this yaml file requires a restart of the app, also you need to have the `session.secret` part to use OIDC (some other providers might diff --git a/docs/auth/okta/provider.md b/docs/auth/okta/provider.md index abc6231839..1e88aa4cf6 100644 --- a/docs/auth/okta/provider.md +++ b/docs/auth/okta/provider.md @@ -45,6 +45,8 @@ auth: audience: ${AUTH_OKTA_DOMAIN} authServerId: ${AUTH_OKTA_AUTH_SERVER_ID} # Optional idp: ${AUTH_OKTA_IDP} # Optional + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # Optional: supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code # https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned additionalScopes: ${AUTH_OKTA_ADDITIONAL_SCOPES} # Optional signIn: @@ -62,6 +64,7 @@ The values referenced are found on the Application page on your Okta site. `https://company.okta.com` - `authServerId`: The authorization server ID for the Application - `idp`: The identity provider for the application, e.g. `0oaulob4BFVa4zQvt0g3` +- `sessionDuration`: Lifespan of the user session. `additionalScopes` is an optional value, a string of space separated scopes, that will be combined with the default `scope` value of `openid profile email offline_access` to adjust the `scope` sent to Okta during OAuth. This will have an impact on [the dependent claims returned](https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned). For example, setting the `additionalScopes` value to `groups` will result in the claim returning a list of the groups that the user is a member of that also match the ID token group filter of the client app. diff --git a/docs/auth/onelogin/provider.md b/docs/auth/onelogin/provider.md index edceb1de62..d3f57f0615 100644 --- a/docs/auth/onelogin/provider.md +++ b/docs/auth/onelogin/provider.md @@ -38,6 +38,8 @@ auth: clientId: ${AUTH_ONELOGIN_CLIENT_ID} clientSecret: ${AUTH_ONELOGIN_CLIENT_SECRET} issuer: https://.onelogin.com/oidc/2 + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/onelogin/provider#resolvers for more resolvers @@ -51,6 +53,10 @@ found on the SSO tab** for the OneLogin Application: - `clientSecret`: The client secret - `issuer`: The issuer URL +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/auth/vmware-cloud/provider.md b/docs/auth/vmware-cloud/provider.md index d654331178..3f747f494e 100644 --- a/docs/auth/vmware-cloud/provider.md +++ b/docs/auth/vmware-cloud/provider.md @@ -47,6 +47,8 @@ auth: development: clientId: ${APP_ID} organizationId: ${ORG_ID} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code signIn: resolvers: # See https://backstage.io/docs/auth/vmware-cloud/provider#resolvers for more resolvers @@ -68,6 +70,10 @@ key for signing session cookies set by Backstage. ::: +### Optional + +- `sessionDuration`: Lifespan of the user session. + ### Resolvers This provider includes several resolvers out of the box that you can use: diff --git a/docs/faq/product.md b/docs/faq/product.md index edbb1ffb4d..f09b5696b8 100644 --- a/docs/faq/product.md +++ b/docs/faq/product.md @@ -1,6 +1,6 @@ --- id: product -title: Product FAQ +title: Non-technical FAQ description: Questions related to product and design. --- diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 1a921103b1..c323d5118a 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -80,11 +80,13 @@ The default value is `false`. #### Internationalization -To customize or translate the **Delete Pod** text, use the following approach: +To customize or translate text in some of the components, use the following approach: ```js import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; import { kubernetesReactTranslationRef } from '@backstage/plugin-kubernetes-react/alpha'; +import { kubernetesTranslationRef } from '@backstage/plugin-kubernetes/alpha'; +import { kubernetesClusterTranslationRef } from '@backstage/plugin-kubernetes-cluster/alpha'; const app = createApp({ __experimentalTranslations: { @@ -94,7 +96,21 @@ const app = createApp({ messages: { "podDrawer.buttons.delete": 'Restart Pod' } - }) + }), + createTranslationMessages({ + ref: kubernetesTranslationRef, + messages: { + 'kubernetesContentPage.permissionAlert.title': 'Insufficient permissions', + 'kubernetesContentPage.permissionAlert.message': 'You do not have permissions to view Kubernetes objects.', + }, + }), + createTranslationMessages({ + ref: kubernetesClusterTranslationRef, + messages: { + 'kubernetesClusterContentPage.permissionAlert.title': 'Insufficient permissions', + 'kubernetesClusterContentPage.permissionAlert.message': 'You do not have permissions to view Kubernetes objects.', + }, + }), ] }, ... diff --git a/docs/features/kubernetes/permissions.md b/docs/features/kubernetes/permissions.md new file mode 100644 index 0000000000..5f1475d4b3 --- /dev/null +++ b/docs/features/kubernetes/permissions.md @@ -0,0 +1,18 @@ +--- +id: permissions +title: Permissions +description: Configuring permissions for Kubernetes plugin +--- + +The Kubernetes plugin integrates with the permission framework. Administrators can define PermissionPolicies +to restrict access to the `/clusters`, `/services/:serviceId`, `/resources` and `/proxy` endpoints. + +This feature assumes your Backstage instance has enabled the [permissions framework](https://backstage.io/docs/permissions/getting-started). + +### Available permissions + +| Name | Policy | Description | +| ------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------- | +| kubernetes.clusters.read | read | Allows the user to read Kubernetes clusters information under `/clusters` | +| kubernetes.resources.read | read | Allows the user to read Kubernetes resources information under `/services/:serviceId` and `/resources` | +| kubernetes.proxy | | Allows the user to make arbitrary requests to the [REST API](https://kubernetes.io/docs/reference/using-api/api-concepts/) under `/proxy` | diff --git a/docs/overview/logos.md b/docs/overview/logos.md index fe993cc378..3198b38b71 100644 --- a/docs/overview/logos.md +++ b/docs/overview/logos.md @@ -7,36 +7,5 @@ description: Guidelines for how to use the Backstage logos and icons Guidelines for how to use the Backstage logo and icon can be found [here](https://backstage.io/logo_assets/Backstage_Identity_Assets_Overview.pdf). -The assets below are all in `.svg` format. Other formats are available in the -[repository](https://github.com/backstage/backstage/tree/master/microsite/static/logo_assets). -## Backstage logo - - - Backstage White logo - - - - Backstage Teal logo - - - - Backstage Black logo - - -## Backstage icon - -
- - Backstage White Icon - - - Backstage Teal Icon - - - Backstage Gradient Icon - - - Backstage Black Icon - -
+You can find the assets at https://github.com/cncf/artwork/tree/main/projects/backstage. diff --git a/docs/overview/threat-model.md b/docs/overview/threat-model.md index 2461f002c8..6f06fbd527 100644 --- a/docs/overview/threat-model.md +++ b/docs/overview/threat-model.md @@ -1,6 +1,7 @@ --- id: threat-model title: Backstage Threat Model +sidebar_label: Security & Thread Model description: A document describing the threat model for Backstage. --- diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 43bde19e35..e07ff68c18 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -405,6 +405,10 @@ const config: Config = { label: 'GitHub', to: 'https://github.com/backstage/', }, + { + label: 'Assets', + to: 'https://github.com/cncf/artwork/tree/main/projects/backstage', + }, ], }, ], diff --git a/microsite/sidebars.js b/microsite/sidebars.js index e18fc5a229..46240cb649 100644 --- a/microsite/sidebars.js +++ b/microsite/sidebars.js @@ -5,14 +5,11 @@ module.exports = { Overview: [ 'overview/what-is-backstage', 'overview/architecture-overview', - 'overview/roadmap', - 'overview/vision', 'overview/background', - 'overview/adopting', - 'overview/versioning-policy', + 'overview/vision', + 'overview/roadmap', 'overview/threat-model', - 'overview/support', - 'overview/logos', + 'overview/versioning-policy', ], 'Getting Started': [ 'getting-started/index', @@ -46,6 +43,7 @@ module.exports = { 'getting-started/create-a-component', ], }, + 'overview/support', 'getting-started/keeping-backstage-updated', ], 'Core Features': [ @@ -494,36 +492,46 @@ module.exports = { ], }, ], - 'Designing for Backstage': [ - 'dls/design', - 'dls/component-design-guidelines', - 'dls/contributing-to-storybook', - 'dls/figma', - ], Tutorials: [ - 'tutorials/quickstart-app-plugin', - 'tutorials/react-router-stable-migration', - 'tutorials/react18-migration', - 'tutorials/package-role-migration', - 'tutorials/migrating-away-from-core', - 'tutorials/configuring-plugin-databases', - 'tutorials/switching-sqlite-postgres', - 'tutorials/using-backstage-proxy-within-plugin', - 'tutorials/yarn-migration', - 'tutorials/migrate-to-mui5', - 'tutorials/auth-service-migration', - 'tutorials/enable-public-entry', - 'tutorials/setup-opentelemetry', + { 'Non-technical': ['overview/adopting'] }, + { + Techical: [ + 'tutorials/quickstart-app-plugin', + 'tutorials/configuring-plugin-databases', + 'tutorials/switching-sqlite-postgres', + 'tutorials/using-backstage-proxy-within-plugin', + 'tutorials/enable-public-entry', + 'tutorials/setup-opentelemetry', + 'accessibility/index', + ], + }, + { + Migrations: [ + 'tutorials/react-router-stable-migration', + 'tutorials/react18-migration', + 'tutorials/package-role-migration', + 'tutorials/migrating-away-from-core', + 'tutorials/yarn-migration', + 'tutorials/migrate-to-mui5', + 'tutorials/auth-service-migration', + ], + }, ], - FAQ: ['faq/index', 'faq/product', 'faq/technical'], - Accessibility: ['accessibility/index'], Contribute: [ 'contribute/index', 'contribute/getting-involved', 'contribute/project-structure', ], References: [ + { + 'Designing for Backstage': [ + 'dls/design', + 'dls/component-design-guidelines', + 'dls/contributing-to-storybook', + 'dls/figma', + ], + }, { type: 'category', label: 'Architecture Decision Records (ADRs)', diff --git a/microsite/src/pages/community/community.module.scss b/microsite/src/pages/community/community.module.scss index 3f829b0100..14ff66db37 100644 --- a/microsite/src/pages/community/community.module.scss +++ b/microsite/src/pages/community/community.module.scss @@ -10,7 +10,7 @@ } p { - text-align: justify; + text-align: left; } } diff --git a/microsite/src/pages/community/index.tsx b/microsite/src/pages/community/index.tsx index 171a5efcdf..1e31edeedc 100644 --- a/microsite/src/pages/community/index.tsx +++ b/microsite/src/pages/community/index.tsx @@ -33,14 +33,14 @@ const Community = () => { }, { content: 'Subscribe to the', - label: 'Community newsletter', + label: 'Community Newsletter', link: 'https://info.backstage.spotify.com/newsletter_subscribe', }, ]; const officialInitiatives: ICollectionItem[] = [ { - title: 'Community sessions', + title: 'Community Sessions', content: 'Maintainers and adopters meet monthly to share updates, demos, and ideas. You can find recorded session on our YouTube channel!', link: 'https://github.com/backstage/community/tree/main/backstage-community-sessions#backstage-community-sessions', @@ -63,6 +63,13 @@ const Community = () => { link: 'https://training.linuxfoundation.org/training/introduction-to-backstage-developer-portals-made-easy-lfs142x/', label: 'Learn more', }, + { + title: 'Certified Backstage Associate (CBA)', + content: + 'Designed for IT engineers, developers, platform engineers, and other IT professionals, the CBA proves you have the skills and the mindset to work with Backstage.', + link: 'https://www.cncf.io/training/certification/cba/', + label: 'Learn more', + }, ]; const partners: { name: string; url: string; logo: string }[] = [ @@ -125,11 +132,10 @@ const Community = () => { > Join the vibrant community around Backstage through social media and different meetups. To ensure that you have a welcoming - environment, we follow the + environment, we follow the{' '} - {' '} - CNCF Code of Conduct{' '} - + CNCF Code of Conduct + {' '} in everything we do. @@ -159,7 +165,7 @@ const Community = () => { -

Offical Backstage initiatives

+

Offical Backstage Initiatives

Stay tuned to the latest developments

diff --git a/microsite/src/pages/demos/demos.module.scss b/microsite/src/pages/demos/demos.module.scss index b99874297e..c8caedbeac 100644 --- a/microsite/src/pages/demos/demos.module.scss +++ b/microsite/src/pages/demos/demos.module.scss @@ -13,7 +13,7 @@ .banner { p { flex: 1; - text-align: justify; + text-align: left; } iframe { diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 071a4a4ba4..5e159be704 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -20,16 +20,12 @@ "license": "Apache-2.0", "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "main": "src/index.ts", "types": "src/index.ts", "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] diff --git a/packages/backend-app-api/report-alpha.api.md b/packages/backend-app-api/report-alpha.api.md deleted file mode 100644 index 163cc325a1..0000000000 --- a/packages/backend-app-api/report-alpha.api.md +++ /dev/null @@ -1,17 +0,0 @@ -## API Report File for "@backstage/backend-app-api" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; - -// @alpha @deprecated (undocumented) -export const featureDiscoveryServiceFactory: ServiceFactory< - FeatureDiscoveryService, - 'root', - 'singleton' ->; - -// (No @packageDocumentation comment for this package) -``` diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts deleted file mode 100644 index 6550a6b1d0..0000000000 --- a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts +++ /dev/null @@ -1,328 +0,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 { - startTestBackend, - mockServices, - createMockDirectory, -} from '@backstage/backend-test-utils'; -import { featureDiscoveryServiceFactory } from './featureDiscoveryServiceFactory'; - -const mockDir = createMockDirectory(); -process.argv[1] = mockDir.path; - -const pluginApiPath = require.resolve('@backstage/backend-plugin-api'); - -describe('featureDiscoveryServiceFactory', () => { - beforeEach(() => { - mockDir.setContent({ - 'package.json': JSON.stringify({ - name: 'example-app', - dependencies: { - 'detected-plugin': '0.0.0', - 'detected-module': '0.0.0', - 'detected-plugin-with-alpha': '0.0.0', - 'detected-library': '0.0.0', - }, - }), - 'node_modules/detected-plugin': { - 'package.json': JSON.stringify({ - name: 'detected-plugin', - main: 'index.js', - backstage: { - role: 'backend-plugin', - }, - }), - 'index.js': ` - const { createBackendPlugin, coreServices } = require('${pluginApiPath}'); - exports.default = createBackendPlugin({ - pluginId: 'detected', - register(env) { - env.registerInit({ - deps: { logger: coreServices.rootLogger }, - async init({ logger }) { - logger.warn('detected-plugin'); - }, - }); - }, - }); - `, - }, - 'node_modules/detected-module': { - 'package.json': JSON.stringify({ - name: 'detected-module', - main: 'index.js', - backstage: { - role: 'backend-plugin-module', - }, - }), - 'index.js': ` - const { createBackendModule, coreServices } = require('${pluginApiPath}'); - exports.default = createBackendModule({ - pluginId: 'detected', - moduleId: 'derp', - register(env) { - env.registerInit({ - deps: { logger: coreServices.rootLogger }, - async init({ logger }) { - logger.warn('detected-module'); - }, - }); - }, - }); - `, - }, - 'node_modules/detected-plugin-with-alpha': { - 'package.json': JSON.stringify({ - name: 'detected-plugin-with-alpha', - main: 'index.js', - exports: { - '.': { - default: 'index.js', - }, - './alpha': { - default: 'alpha.js', - }, - './package.json': './package.json', - }, - backstage: { - role: 'backend-plugin', - }, - }), - 'index.js': `exports.default = undefined;`, - 'alpha.js': ` - const { createBackendPlugin, coreServices } = require('${pluginApiPath}'); - exports.default = createBackendPlugin({ - pluginId: 'detected-alpha', - register(env) { - env.registerInit({ - deps: { logger: coreServices.rootLogger }, - async init({ logger }) { - logger.warn('detected-plugin-with-alpha'); - }, - }); - }, - }); - `, - }, - 'node_modules/detected-library': { - 'package.json': JSON.stringify({ - name: 'detected-library', - main: 'index.js', - backstage: { - role: 'node-library', - }, - }), - 'index.js': ` - const { createServiceFactory, createServiceRef, coreServices } = require('${pluginApiPath}'); - exports.default = createServiceFactory({ - service: createServiceRef({ id: 'test', scope: 'root' }), - deps: { logger: coreServices.rootLogger }, - factory({ logger }) { - logger.warn('detected-library'); - return {}; - }, - }); - `, - }, - }); - }); - - it('should detect plugin and module packages when "all" is specified', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { backend: { packages: 'all' } }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - }); - - it('detects only the packages that are listed as included', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - include: [ - 'detected-plugin', - 'detected-plugin-with-alpha', - 'detected-library', - ], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - }); - - it('does not detect packages when included is an empty list', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - include: [], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-library'); - }); - - it('does not detect an excluded packages', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - exclude: ['detected-plugin', 'detected-module'], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - }); - - it('does not excluded packages when it is an empty list', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - exclude: [], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - expect(mock.warn).toHaveBeenCalledWith('detected-library'); - }); - - it('does not detect packages that are included and excluded', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - packages: { - include: [ - 'detected-plugin', - 'detected-module', - 'detected-plugin-with-alpha', - ], - exclude: ['detected-module'], - }, - }, - }, - }), - ], - }); - - expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-library'); - expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); - expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); - }); - - it('does not detect any packages when "packages" is empty', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { backend: { packages: {} } }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalled(); - }); - - it('does not detect any packages when "packages" is not present', async () => { - const mock = mockServices.rootLogger.mock({ child: () => mock }); - - await startTestBackend({ - features: [ - mock.factory, - featureDiscoveryServiceFactory, - mockServices.rootConfig.factory({ - data: { backend: {} }, - }), - ], - }); - - expect(mock.warn).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts deleted file mode 100644 index 05de659083..0000000000 --- a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts +++ /dev/null @@ -1,38 +0,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 { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { PackageDiscoveryService } from '../../../backend-defaults/src/PackageDiscoveryService'; - -/** - * @alpha - * @deprecated The `featureDiscoveryServiceFactory` is deprecated in favor of using {@link @backstage/backend-defaults#discoveryFeatureLoader} instead. - */ -export const featureDiscoveryServiceFactory = createServiceFactory({ - service: featureDiscoveryServiceRef, - deps: { - config: coreServices.rootConfig, - logger: coreServices.rootLogger, - }, - factory({ config, logger }) { - return new PackageDiscoveryService(config, logger); - }, -}); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 6bec5439ec..02fad4f217 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -37,7 +37,6 @@ import type { InternalServiceFactory } from '../../../backend-plugin-api/src/ser import { ForwardedError, ConflictError, assertError } from '@backstage/errors'; import { instanceMetadataServiceRef, - featureDiscoveryServiceRef, BackendFeatureMeta, } from '@backstage/backend-plugin-api/alpha'; import { DependencyGraph } from '../lib/DependencyGraph'; @@ -252,20 +251,6 @@ export class BackendInitializer { this.#addFeature(await feature); } - const featureDiscovery = await this.#serviceRegistry.get( - // TODO: Let's leave this in place and remove it once the deprecated service is removed. We can do that post-1.0 since it's alpha - featureDiscoveryServiceRef, - 'root', - ); - - if (featureDiscovery) { - const { features } = await featureDiscovery.getBackendFeatures(); - for (const feature of features) { - this.#addFeature(unwrapFeature(feature)); - } - this.#serviceRegistry.checkForCircularDeps(); - } - await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders); this.#serviceRegistry.add( diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index aa97313921..438790a177 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -429,6 +429,10 @@ export interface Config { * The instance connection name for the cloudsql instance, e.g. `project:region:instance` */ instance: string; + /** + * The ip address type to use for the connection. Defaults to 'PUBLIC' + */ + ipAddressType?: 'PUBLIC' | 'PRIVATE' | 'PSC'; } | { /** diff --git a/packages/backend-defaults/src/PackageDiscoveryService.ts b/packages/backend-defaults/src/PackageDiscoveryService.ts index d7d127f4fa..90e8d0f59d 100644 --- a/packages/backend-defaults/src/PackageDiscoveryService.ts +++ b/packages/backend-defaults/src/PackageDiscoveryService.ts @@ -22,7 +22,6 @@ import { RootConfigService, RootLoggerService, } from '@backstage/backend-plugin-api'; -import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; import { BackstagePackageJson } from '@backstage/cli-node'; const DETECTED_PACKAGE_ROLES = [ @@ -79,7 +78,7 @@ async function findClosestPackageDir( } /** @internal */ -export class PackageDiscoveryService implements FeatureDiscoveryService { +export class PackageDiscoveryService { constructor( private readonly config: RootConfigService, private readonly logger: RootLoggerService, diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts index 1a3691c83b..254bdf6c9d 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts @@ -221,6 +221,63 @@ describe('postgres', () => { }); }); + it('passes default settings to cloud-sql-connector', async () => { + const { Connector } = jest.requireMock( + '@google-cloud/cloud-sql-connector', + ) as jest.Mocked; + + const mockStream = (): any => {}; + Connector.prototype.getOptions.mockResolvedValue({ stream: mockStream }); + + await buildPgDatabaseConfig( + new ConfigReader({ + client: 'pg', + connection: { + type: 'cloudsql', + user: 'ben@gke.com', + instance: 'project:region:instance', + port: 5423, + }, + }), + { connection: { database: 'other_db' } }, + ); + + expect(Connector.prototype.getOptions).toHaveBeenCalledWith({ + authType: 'IAM', + instanceConnectionName: 'project:region:instance', + ipType: 'PUBLIC', + }); + }); + + it('passes ip settings to cloud-sql-connector', async () => { + const { Connector } = jest.requireMock( + '@google-cloud/cloud-sql-connector', + ) as jest.Mocked; + + const mockStream = (): any => {}; + Connector.prototype.getOptions.mockResolvedValue({ stream: mockStream }); + + await buildPgDatabaseConfig( + new ConfigReader({ + client: 'pg', + connection: { + type: 'cloudsql', + user: 'ben@gke.com', + instance: 'project:region:instance', + ipAddressType: 'PRIVATE', + port: 5423, + }, + }), + { connection: { database: 'other_db' } }, + ); + + expect(Connector.prototype.getOptions).toHaveBeenCalledWith({ + authType: 'IAM', + instanceConnectionName: 'project:region:instance', + ipType: 'PRIVATE', + }); + }); + it('throws an error when the connection type is not supported', async () => { await expect( buildPgDatabaseConfig( diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts index 3583049a7d..4adf77a2f6 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts @@ -107,7 +107,7 @@ export async function buildPgDatabaseConfig( const connector = new CloudSqlConnector(); const clientOpts = await connector.getOptions({ instanceConnectionName: config.connection.instance, - ipType: IpAddressTypes.PUBLIC, + ipType: config.connection.ipAddressType ?? IpAddressTypes.PUBLIC, authType: AuthTypes.IAM, }); diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index ac03cceafa..2464b4cd14 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -45,7 +45,6 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "^0.25.0", "@backstage/backend-defaults": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", @@ -74,6 +73,7 @@ "winston": "^3.2.1" }, "devDependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-app-backend": "workspace:^", diff --git a/packages/backend-dynamic-feature-service/report.api.md b/packages/backend-dynamic-feature-service/report.api.md index e2ad34c84d..b0fa84edbd 100644 --- a/packages/backend-dynamic-feature-service/report.api.md +++ b/packages/backend-dynamic-feature-service/report.api.md @@ -13,7 +13,6 @@ import { DiscoveryService } from '@backstage/backend-plugin-api'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventsBackend } from '@backstage/plugin-events-backend'; import { EventsService } from '@backstage/plugin-events-node'; -import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { IndexBuilder } from '@backstage/plugin-search-backend-node'; @@ -150,13 +149,6 @@ export interface DynamicPluginsFactoryOptions { // @public @deprecated (undocumented) export const dynamicPluginsFeatureDiscoveryLoader: BackendFeature; -// @public @deprecated (undocumented) -export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactory< - FeatureDiscoveryService, - 'root', - 'singleton' ->; - // @public export const dynamicPluginsFeatureLoader: (( options?: DynamicPluginsFeatureLoaderOptions, diff --git a/packages/backend-dynamic-feature-service/src/manager/index.ts b/packages/backend-dynamic-feature-service/src/manager/index.ts index d24a41a7a3..391ea46249 100644 --- a/packages/backend-dynamic-feature-service/src/manager/index.ts +++ b/packages/backend-dynamic-feature-service/src/manager/index.ts @@ -32,7 +32,6 @@ export type { export { DynamicPluginManager, - dynamicPluginsFeatureDiscoveryServiceFactory, dynamicPluginsServiceFactory, dynamicPluginsServiceFactoryWithOptions, dynamicPluginsServiceRef, diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts index 398e9fb14c..0162cbef2d 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts @@ -37,10 +37,6 @@ import { import { PackageRole, PackageRoles } from '@backstage/cli-node'; import { findPaths } from '@backstage/cli-common'; import * as fs from 'fs'; -import { - FeatureDiscoveryService, - featureDiscoveryServiceRef, -} from '@backstage/backend-plugin-api/alpha'; /** * @public @@ -307,55 +303,27 @@ export const dynamicPluginsServiceFactory = Object.assign( dynamicPluginsServiceFactoryWithOptions(), ); -class DynamicPluginsEnabledFeatureDiscoveryService - implements FeatureDiscoveryService -{ - constructor( - private readonly dynamicPlugins: DynamicPluginProvider, - private readonly featureDiscoveryService?: FeatureDiscoveryService, - ) {} +class DynamicPluginsEnabledFeatureDiscoveryService { + constructor(private readonly dynamicPlugins: DynamicPluginProvider) {} async getBackendFeatures(): Promise<{ features: Array }> { - const staticFeatures = - (await this.featureDiscoveryService?.getBackendFeatures())?.features ?? - []; - return { - features: [ - ...this.dynamicPlugins - .backendPlugins() - .flatMap((plugin): BackendFeature[] => { - if (plugin.installer?.kind === 'new') { - const installed = plugin.installer.install(); - if (Array.isArray(installed)) { - return installed; - } - return [installed]; + features: this.dynamicPlugins + .backendPlugins() + .flatMap((plugin): BackendFeature[] => { + if (plugin.installer?.kind === 'new') { + const installed = plugin.installer.install(); + if (Array.isArray(installed)) { + return installed; } - return []; - }), - ...staticFeatures, - ], + return [installed]; + } + return []; + }), }; } } -/** - * @public - * @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins. - */ -export const dynamicPluginsFeatureDiscoveryServiceFactory = - createServiceFactory({ - service: featureDiscoveryServiceRef, - deps: { - config: coreServices.rootConfig, - dynamicPlugins: dynamicPluginsServiceRef, - }, - factory({ dynamicPlugins }) { - return new DynamicPluginsEnabledFeatureDiscoveryService(dynamicPlugins); - }, - }); - /** * @public * @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins. diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index 8509d1196d..d485e2132e 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -3,7 +3,6 @@ > 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 { ServiceRef } from '@backstage/backend-plugin-api'; // @alpha (undocumented) @@ -18,21 +17,6 @@ export type BackendFeatureMeta = moduleId: string; }; -// @alpha (undocumented) -export interface FeatureDiscoveryService { - // (undocumented) - getBackendFeatures(): Promise<{ - features: Array; - }>; -} - -// @alpha @deprecated -export const featureDiscoveryServiceRef: ServiceRef< - FeatureDiscoveryService, - 'root', - 'singleton' ->; - // @alpha (undocumented) export interface InstanceMetadataService { // (undocumented) diff --git a/packages/backend-plugin-api/src/alpha.ts b/packages/backend-plugin-api/src/alpha.ts index 1a39fe6c23..6e91e10671 100644 --- a/packages/backend-plugin-api/src/alpha.ts +++ b/packages/backend-plugin-api/src/alpha.ts @@ -14,26 +14,7 @@ * limitations under the License. */ -import { - BackendFeature, - createServiceRef, -} from '@backstage/backend-plugin-api'; - -/** @alpha */ -export interface FeatureDiscoveryService { - getBackendFeatures(): Promise<{ features: Array }>; -} - -/** - * An optional service that can be used to dynamically load in additional BackendFeatures at runtime. - * @alpha - * @deprecated The `featureDiscoveryServiceRef` is deprecated in favor of using {@link @backstage/backend-defaults#discoveryFeatureLoader} instead. - */ -export const featureDiscoveryServiceRef = - createServiceRef({ - id: 'core.featureDiscovery', - scope: 'root', - }); +import { createServiceRef } from '@backstage/backend-plugin-api'; /** * EXPERIMENTAL: Instance metadata service. diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index bec25ea251..0c13469866 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -12,6 +12,9 @@ export type BackstagePackage = { packageJson: BackstagePackageJson; }; +// @public (undocumented) +export type BackstagePackageFeatureType = (typeof packageFeatureType)[number]; + // @public export interface BackstagePackageJson { // (undocumented) @@ -22,6 +25,7 @@ export interface BackstagePackageJson { pluginId?: string | null; pluginPackage?: string; pluginPackages?: string[]; + features?: Record; }; // (undocumented) bundled?: boolean; @@ -112,6 +116,14 @@ export type LockfileDiffEntry = { range: string; }; +// @public +export const packageFeatureType: readonly [ + '@backstage/BackendFeature', + '@backstage/BackstagePlugin', + '@backstage/FrontendPlugin', + '@backstage/FrontendModule', +]; + // @public export class PackageGraph extends Map { collectPackageNames( diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index 1da896a057..2f82af0808 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -22,6 +22,24 @@ import { GitUtils } from '../git'; import { Lockfile } from './Lockfile'; import { JsonValue } from '@backstage/types'; +/** + * A list of the feature types we want to extract from the project + * and include in the metadata + * + * @public + */ +export const packageFeatureType = [ + '@backstage/BackendFeature', + '@backstage/BackstagePlugin', + '@backstage/FrontendPlugin', + '@backstage/FrontendModule', +] as const; + +/** + * @public + */ +export type BackstagePackageFeatureType = (typeof packageFeatureType)[number]; + /** * Known fields in Backstage package.json files. * @@ -72,6 +90,11 @@ export interface BackstagePackageJson { * All packages that are part of the plugin. Must always and only be set for plugin packages and plugin library packages. */ pluginPackages?: string[]; + + /** + * The feature types exported from the package, indexed by path. + */ + features?: Record; }; exports?: JsonValue; diff --git a/packages/cli-node/src/monorepo/index.ts b/packages/cli-node/src/monorepo/index.ts index fdb8395c16..14630356b9 100644 --- a/packages/cli-node/src/monorepo/index.ts +++ b/packages/cli-node/src/monorepo/index.ts @@ -16,10 +16,12 @@ export { isMonoRepo } from './isMonoRepo'; export { + packageFeatureType, PackageGraph, type PackageGraphNode, type BackstagePackage, type BackstagePackageJson, + type BackstagePackageFeatureType, } from './PackageGraph'; export { Lockfile, diff --git a/packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts b/packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts index a12a48c523..054e651f0e 100644 --- a/packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts +++ b/packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { PackageRole } from '@backstage/cli-node'; +import { PackageRole, BackstagePackageFeatureType } from '@backstage/cli-node'; import { Project } from 'ts-morph'; -import { BackstagePackageFeatureType } from '../typeDistProject'; const mockEntryPoint = 'dist/index.d.ts'; diff --git a/packages/cli/src/lib/packager/productionPack.ts b/packages/cli/src/lib/packager/productionPack.ts index 9736e97b8d..bfbafc9d27 100644 --- a/packages/cli/src/lib/packager/productionPack.ts +++ b/packages/cli/src/lib/packager/productionPack.ts @@ -188,7 +188,16 @@ async function rewriteEntryPoints( // This ensures that the `backstage` field is at the top of the // `exports` field in the package.json because order is important. // https://nodejs.org/docs/latest-v20.x/api/packages.html#conditional-exports + // + // Adding this to the `exports` field in the package.json is to temporarily + // support any existing behavior that relies on this, however not all packages + // have exports field in their package.json. exp = { backstage: defaultFeatureType, ...exp }; + + // Add the default feature type to the backstage metadata in the package.json + pkg.backstage = pkg.backstage ?? {}; + pkg.backstage.features = pkg.backstage.features ?? {}; + pkg.backstage.features[entryPoint.mount] = defaultFeatureType; } } diff --git a/packages/cli/src/lib/typeDistProject.test.ts b/packages/cli/src/lib/typeDistProject.test.ts index dfe3e68ec0..6eef53816b 100644 --- a/packages/cli/src/lib/typeDistProject.test.ts +++ b/packages/cli/src/lib/typeDistProject.test.ts @@ -14,12 +14,9 @@ * limitations under the License. */ -import { PackageRole } from '@backstage/cli-node'; +import { PackageRole, BackstagePackageFeatureType } from '@backstage/cli-node'; import createFeatureEnvironment from './__testUtils__/createFeatureEnvironment'; -import { - getEntryPointDefaultFeatureType, - BackstagePackageFeatureType, -} from './typeDistProject'; +import { getEntryPointDefaultFeatureType } from './typeDistProject'; describe('typeDistProject', () => { describe('for package role', () => { diff --git a/packages/cli/src/lib/typeDistProject.ts b/packages/cli/src/lib/typeDistProject.ts index 59dc503f22..77d6f4a836 100644 --- a/packages/cli/src/lib/typeDistProject.ts +++ b/packages/cli/src/lib/typeDistProject.ts @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PackageRole } from '@backstage/cli-node'; +import { + BackstagePackageFeatureType, + packageFeatureType, + PackageRole, +} from '@backstage/cli-node'; import { resolve as resolvePath } from 'path'; import { Project, SourceFile, SyntaxKind, ts, Type } from 'ts-morph'; import { paths } from './paths'; @@ -35,17 +39,6 @@ const targetPackageRoles: PackageRole[] = [ 'node-library', ]; -// A list of the feature types we want to extract from the project -// and include in the metadata -const targetFeatureTypes = [ - '@backstage/BackendFeature', - '@backstage/BackstagePlugin', - '@backstage/FrontendPlugin', - '@backstage/FrontendModule', -] as const; - -export type BackstagePackageFeatureType = (typeof targetFeatureTypes)[number]; - export const getEntryPointDefaultFeatureType = ( role: PackageRole, packageDir: string, @@ -146,6 +139,6 @@ function isTargetFeatureType( type: string | BackstagePackageFeatureType, ): type is BackstagePackageFeatureType { return ( - !!type && targetFeatureTypes.includes(type as BackstagePackageFeatureType) + !!type && packageFeatureType.includes(type as BackstagePackageFeatureType) ); } diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 6a84d11eee..55b0d72c6a 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -59,6 +59,7 @@ "@backstage/errors": "workspace:^", "@backstage/theme": "workspace:^", "@backstage/version-bridge": "workspace:^", + "@dagrejs/dagre": "^1.1.4", "@date-io/core": "^1.3.13", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", @@ -72,7 +73,6 @@ "d3-selection": "^3.0.0", "d3-shape": "^3.0.0", "d3-zoom": "^3.0.0", - "dagre": "^0.8.5", "js-yaml": "^4.1.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", @@ -106,7 +106,6 @@ "@types/d3-selection": "^3.0.1", "@types/d3-shape": "^3.0.1", "@types/d3-zoom": "^3.0.1", - "@types/dagre": "^0.7.44", "@types/google-protobuf": "^3.7.2", "@types/react": "^18.0.0", "@types/react-helmet": "^6.1.0", diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index ee6b74f97a..770f1c9c23 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -18,7 +18,7 @@ import React from 'react'; import * as d3Zoom from 'd3-zoom'; import * as d3Selection from 'd3-selection'; import useTheme from '@material-ui/core/styles/useTheme'; -import dagre from 'dagre'; +import dagre from '@dagrejs/dagre'; import debounce from 'lodash/debounce'; import { DependencyGraphTypes as Types } from './types'; import { Node } from './Node'; diff --git a/packages/core-components/src/components/DependencyGraph/Edge.tsx b/packages/core-components/src/components/DependencyGraph/Edge.tsx index 6a1f8861bf..cd8dd0642a 100644 --- a/packages/core-components/src/components/DependencyGraph/Edge.tsx +++ b/packages/core-components/src/components/DependencyGraph/Edge.tsx @@ -21,7 +21,7 @@ import makeStyles from '@material-ui/core/styles/makeStyles'; import { DependencyGraphTypes as Types } from './types'; import { ARROW_MARKER_ID, EDGE_TEST_ID, LABEL_TEST_ID } from './constants'; import { DefaultLabel } from './DefaultLabel'; -import dagre from 'dagre'; +import dagre from '@dagrejs/dagre'; /* Based on: https://github.com/dagrejs/dagre/wiki#configuring-the-layout */ export type EdgeProperties = { diff --git a/packages/core-components/src/components/DependencyGraph/Node.test.tsx b/packages/core-components/src/components/DependencyGraph/Node.test.tsx index e1af58e731..a507213495 100644 --- a/packages/core-components/src/components/DependencyGraph/Node.test.tsx +++ b/packages/core-components/src/components/DependencyGraph/Node.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import dagre from 'dagre'; +import dagre from '@dagrejs/dagre'; import { render } from '@testing-library/react'; import { Node } from './Node'; import { DependencyGraphTypes as Types } from './types'; diff --git a/packages/core-components/src/components/DependencyGraph/Node.tsx b/packages/core-components/src/components/DependencyGraph/Node.tsx index 133f45aed9..aac1cd81aa 100644 --- a/packages/core-components/src/components/DependencyGraph/Node.tsx +++ b/packages/core-components/src/components/DependencyGraph/Node.tsx @@ -19,7 +19,7 @@ import makeStyles from '@material-ui/core/styles/makeStyles'; import { DefaultNode } from './DefaultNode'; import { DependencyGraphTypes as Types } from './types'; import { NODE_TEST_ID } from './constants'; -import dagre from 'dagre'; +import dagre from '@dagrejs/dagre'; /** @public */ export type DependencyGraphNodeClassKey = 'node'; diff --git a/plugins/auth-backend-module-atlassian-provider/config.d.ts b/plugins/auth-backend-module-atlassian-provider/config.d.ts index b1edea3d6c..f6433c123e 100644 --- a/plugins/auth-backend-module-atlassian-provider/config.d.ts +++ b/plugins/auth-backend-module-atlassian-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -38,6 +40,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index 784e4b141f..260f805228 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-auth0-provider/config.d.ts b/plugins/auth-backend-module-auth0-provider/config.d.ts index a0ec7d4eac..661409a592 100644 --- a/plugins/auth-backend-module-auth0-provider/config.d.ts +++ b/plugins/auth-backend-module-auth0-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -30,6 +32,7 @@ export interface Config { audience?: string; connection?: string; connectionScope?: string; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-auth0-provider/package.json b/plugins/auth-backend-module-auth0-provider/package.json index 5e1031e9e2..5151c6374f 100644 --- a/plugins/auth-backend-module-auth0-provider/package.json +++ b/plugins/auth-backend-module-auth0-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "@types/passport-auth0": "^1.0.5", "@types/passport-oauth2": "^1.4.15", "supertest": "^7.0.0" diff --git a/plugins/auth-backend-module-aws-alb-provider/config.d.ts b/plugins/auth-backend-module-aws-alb-provider/config.d.ts index 2e38490b28..41c84872de 100644 --- a/plugins/auth-backend-module-aws-alb-provider/config.d.ts +++ b/plugins/auth-backend-module-aws-alb-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -48,6 +50,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index 038c3a90ef..45c3073585 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -48,6 +48,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/types": "workspace:^", "express": "^4.18.2", "msw": "^2.0.8" } diff --git a/plugins/auth-backend-module-bitbucket-provider/config.d.ts b/plugins/auth-backend-module-bitbucket-provider/config.d.ts index 552a66d700..f3d82608bd 100644 --- a/plugins/auth-backend-module-bitbucket-provider/config.d.ts +++ b/plugins/auth-backend-module-bitbucket-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -36,6 +38,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index 0bc5de61ca..bd9792d754 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts b/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts index ee924f47f0..8e63581625 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts +++ b/plugins/auth-backend-module-bitbucket-server-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -27,6 +29,7 @@ export interface Config { clientSecret: string; host: string; callbackUrl?: string; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-bitbucket-server-provider/package.json b/plugins/auth-backend-module-bitbucket-server-provider/package.json index e79bc5dca8..5d52eeff62 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-server-provider/package.json @@ -44,6 +44,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "@types/passport-oauth2": "^1.4.15", "supertest": "^7.0.0" }, diff --git a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts index b73c8d87a5..39a4ca3eb6 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts @@ -43,6 +43,7 @@ export interface Config { * The backstage token expiration. */ backstageTokenExpiration?: HumanDuration | string; + sessionDuration?: HumanDuration | string; }; }; } diff --git a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts index d4fe015b7c..d99d890b50 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts +++ b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -43,6 +45,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-github-provider/config.d.ts b/plugins/auth-backend-module-github-provider/config.d.ts index b1bc0dc477..69abfa0d12 100644 --- a/plugins/auth-backend-module-github-provider/config.d.ts +++ b/plugins/auth-backend-module-github-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -38,6 +40,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index 035bbd8076..bb098ef574 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -43,6 +43,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-gitlab-provider/config.d.ts b/plugins/auth-backend-module-gitlab-provider/config.d.ts index cbb9f01e02..8d47421255 100644 --- a/plugins/auth-backend-module-gitlab-provider/config.d.ts +++ b/plugins/auth-backend-module-gitlab-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -38,6 +40,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index c14d9a2e7a..0a3f2bc19f 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-google-provider/config.d.ts b/plugins/auth-backend-module-google-provider/config.d.ts index 1571c06deb..ec3cd92b31 100644 --- a/plugins/auth-backend-module-google-provider/config.d.ts +++ b/plugins/auth-backend-module-google-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { /** Configuration options for the auth plugin */ auth?: { @@ -38,6 +40,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index c4c3130bbf..8cfacefb6f 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -47,6 +47,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "@types/passport-google-oauth20": "^2.0.3", "supertest": "^7.0.0" }, diff --git a/plugins/auth-backend-module-microsoft-provider/config.d.ts b/plugins/auth-backend-module-microsoft-provider/config.d.ts index 070b6300f7..d69637e7a2 100644 --- a/plugins/auth-backend-module-microsoft-provider/config.d.ts +++ b/plugins/auth-backend-module-microsoft-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -40,6 +42,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index b578ee4428..07185af768 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -46,6 +46,7 @@ "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "@types/passport-microsoft": "^1.0.0", "msw": "^1.0.0", "supertest": "^7.0.0" diff --git a/plugins/auth-backend-module-oauth2-provider/config.d.ts b/plugins/auth-backend-module-oauth2-provider/config.d.ts index 5cee3a5407..ba047e3340 100644 --- a/plugins/auth-backend-module-oauth2-provider/config.d.ts +++ b/plugins/auth-backend-module-oauth2-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -42,6 +44,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index ba498c34e8..68a966323f 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -44,6 +44,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-oidc-provider/config.d.ts b/plugins/auth-backend-module-oidc-provider/config.d.ts index d59c40697a..409c658c23 100644 --- a/plugins/auth-backend-module-oidc-provider/config.d.ts +++ b/plugins/auth-backend-module-oidc-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -40,6 +42,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index 79a89d1a65..d78cb46acd 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -46,6 +46,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/types": "workspace:^", "cookie-parser": "^1.4.6", "express-promise-router": "^4.1.1", "express-session": "^1.17.3", diff --git a/plugins/auth-backend-module-okta-provider/config.d.ts b/plugins/auth-backend-module-okta-provider/config.d.ts index d2b0c4aa02..3f27e805ec 100644 --- a/plugins/auth-backend-module-okta-provider/config.d.ts +++ b/plugins/auth-backend-module-okta-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -40,6 +42,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index 3850cf0e78..9752848720 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-onelogin-provider/config.d.ts b/plugins/auth-backend-module-onelogin-provider/config.d.ts index da7c312dd3..58f471bafb 100644 --- a/plugins/auth-backend-module-onelogin-provider/config.d.ts +++ b/plugins/auth-backend-module-onelogin-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -37,6 +39,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index 0d0a395e11..e004107742 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -45,6 +45,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend-module-pinniped-provider/package.json b/plugins/auth-backend-module-pinniped-provider/package.json index f382ddae29..e00e2abecf 100644 --- a/plugins/auth-backend-module-pinniped-provider/package.json +++ b/plugins/auth-backend-module-pinniped-provider/package.json @@ -36,6 +36,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", + "@backstage/types": "workspace:^", "luxon": "^3.4.3", "openid-client": "^5.4.3" }, diff --git a/plugins/auth-backend-module-pinniped-provider/src/config.d.ts b/plugins/auth-backend-module-pinniped-provider/src/config.d.ts index 50685abfb0..242de7b2b9 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/config.d.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { /** Configuration options for the auth plugin */ auth?: { @@ -27,6 +29,7 @@ export interface Config { */ clientSecret: string; scope?: string; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts index 67db735713..9886d69a41 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { auth?: { providers?: { @@ -34,6 +36,7 @@ export interface Config { | { resolver: 'emailMatchingUserEntityProfileEmail' } >; }; + sessionDuration?: HumanDuration | string; }; }; }; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index 9611b84cab..9eb8f818d1 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -47,6 +47,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/types": "workspace:^", "msw": "^2.0.8", "supertest": "^7.0.0" } diff --git a/plugins/auth-node/src/oauth/OAuthCookieManager.ts b/plugins/auth-node/src/oauth/OAuthCookieManager.ts index d594d7e78b..8d1c4787cc 100644 --- a/plugins/auth-node/src/oauth/OAuthCookieManager.ts +++ b/plugins/auth-node/src/oauth/OAuthCookieManager.ts @@ -16,6 +16,7 @@ import { CookieOptions, Request, Response } from 'express'; import { CookieConfigurer } from '../types'; +import { HumanDuration, durationToMilliseconds } from '@backstage/types'; const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; const TEN_MINUTES_MS = 600 * 1000; @@ -55,6 +56,7 @@ export class OAuthCookieManager { private readonly nonceCookie: string; private readonly refreshTokenCookie: string; private readonly grantedScopeCookie: string; + private readonly maxAge: number; constructor( private readonly options: { @@ -63,6 +65,7 @@ export class OAuthCookieManager { baseUrl: string; callbackUrl: string; cookieConfigurer?: CookieConfigurer; + sessionDuration?: HumanDuration; }, ) { this.cookieConfigurer = options.cookieConfigurer ?? defaultCookieConfigurer; @@ -70,6 +73,9 @@ export class OAuthCookieManager { this.nonceCookie = `${options.providerId}-nonce`; this.refreshTokenCookie = `${options.providerId}-refresh-token`; this.grantedScopeCookie = `${options.providerId}-granted-scope`; + this.maxAge = options.sessionDuration + ? durationToMilliseconds(options.sessionDuration) + : THOUSAND_DAYS_MS; } private getConfig(origin?: string, pathSuffix: string = '') { @@ -103,7 +109,7 @@ export class OAuthCookieManager { res, this.refreshTokenCookie, refreshToken, - THOUSAND_DAYS_MS, + this.maxAge, origin, ); } @@ -117,13 +123,7 @@ export class OAuthCookieManager { } setGrantedScopes(res: Response, scope: string, origin?: string): void { - this.setCookie( - res, - this.grantedScopeCookie, - scope, - THOUSAND_DAYS_MS, - origin, - ); + this.setCookie(res, this.grantedScopeCookie, scope, this.maxAge, origin); } getNonce(req: Request): string | undefined { diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts index 46fa058b47..5bc9fa2551 100644 --- a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts @@ -813,6 +813,75 @@ describe('createOAuthRouteHandlers', () => { }, }); }); + + it('should set sessionDuration to configured value', async () => { + const baseConfigWithSessionDuration = { + ...baseConfig, + config: new ConfigReader({ + sessionDuration: { days: 7 }, + }), + }; + + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfigWithSessionDuration)), + ); + + 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, refreshToken: 'new-refresh-token' }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(200); + const expectedExpirationDate = Date.now() + 7 * 24 * 60 * 60 * 1000; + const cookie = getRefreshTokenCookie(agent); + expect(cookie.expiration_date).toBeGreaterThanOrEqual( + expectedExpirationDate - 1000, + ); + expect(cookie.expiration_date).toBeLessThanOrEqual( + expectedExpirationDate + 1000, + ); + }); + + it('should set sessionDuration to default of 1000 days when not configured', 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, refreshToken: 'new-refresh-token' }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(200); + const expectedExpirationDate = Date.now() + 1000 * 24 * 60 * 60 * 1000; + const cookie = getRefreshTokenCookie(agent); + expect(cookie.expiration_date).toBeGreaterThanOrEqual( + expectedExpirationDate - 1000, + ); + expect(cookie.expiration_date).toBeLessThanOrEqual( + expectedExpirationDate + 1000, + ); + }); }); describe('logout', () => { diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts index 53ae4101d3..72f45d8ab1 100644 --- a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts @@ -40,7 +40,7 @@ import { SignInResolver, } from '../types'; import { OAuthAuthenticator, OAuthAuthenticatorResult } from './types'; -import { Config } from '@backstage/config'; +import { Config, readDurationFromConfig } from '@backstage/config'; import { CookieScopeManager } from './CookieScopeManager'; /** @public */ @@ -99,6 +99,9 @@ export function createOAuthRouteHandlers( const callbackUrl = config.getOptionalString('callbackUrl') ?? `${baseUrl}/${providerId}/handler/frame`; + const sessionDuration = config.has('sessionDuration') + ? readDurationFromConfig(config, { key: 'sessionDuration' }) + : undefined; const stateTransform = options.stateTransform ?? (state => ({ state })); const profileTransform = @@ -110,6 +113,7 @@ export function createOAuthRouteHandlers( defaultAppOrigin, providerId, cookieConfigurer, + sessionDuration, }); const scopeManager = CookieScopeManager.create({ diff --git a/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx b/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx index 5fe4f65136..cfcd47b8f3 100644 --- a/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx +++ b/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx @@ -13,19 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import Grid from '@material-ui/core/Grid'; +import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; -import IconButton from '@material-ui/core/IconButton'; -import SettingsIcon from '@material-ui/icons/Settings'; import DeleteIcon from '@material-ui/icons/Delete'; -import React from 'react'; -import { Widget } from './types'; +import SettingsIcon from '@material-ui/icons/Settings'; import { withTheme } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import validator from '@rjsf/validator-ajv8'; +import React from 'react'; + +import { Widget } from './types'; const Form = withTheme(MuiTheme); @@ -64,13 +68,15 @@ export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => { const [settingsDialogOpen, setSettingsDialogOpen] = React.useState(false); const styles = useStyles(); + const onClose = () => setSettingsDialogOpen(false); + return (
{widget.settingsSchema && ( setSettingsDialogOpen(false)} + onClose={onClose} >
{ experimental_defaultFormStateBehavior={{ allOf: 'populateDefaults', }} - /> + > + + + + +
)} diff --git a/plugins/kubernetes-backend/src/auth/requirePermission.ts b/plugins/kubernetes-backend/src/auth/requirePermission.ts new file mode 100644 index 0000000000..9934ed653d --- /dev/null +++ b/plugins/kubernetes-backend/src/auth/requirePermission.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { + HttpAuthService, + PermissionsService, +} from '@backstage/backend-plugin-api'; +import { NotAllowedError } from '@backstage/errors'; +import { + AuthorizeResult, + type BasicPermission, +} from '@backstage/plugin-permission-common'; + +import express from 'express'; + +export async function requirePermission( + permissionApi: PermissionsService, + permissionRequired: BasicPermission, + httpAuth: HttpAuthService, + req: express.Request, +) { + const decision = ( + await permissionApi.authorize( + [ + { + permission: permissionRequired, + }, + ], + { + credentials: await httpAuth.credentials(req), + }, + ) + )[0]; + + if (decision.result === AuthorizeResult.ALLOW) { + return; + } + throw new NotAllowedError('Unauthorized'); +} diff --git a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts index 7ace17e805..462312a404 100644 --- a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts +++ b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts @@ -18,17 +18,48 @@ import request from 'supertest'; import { mockCredentials, mockServices, + type ServiceMock, startTestBackend, } from '@backstage/backend-test-utils'; import { kubernetesObjectsProviderExtensionPoint } from '@backstage/plugin-kubernetes-node'; -import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + createBackendModule, + type PermissionsService, +} from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; describe('resourcesRoutes', () => { let app: ExtendedHttpServer; + const permissionsMock: ServiceMock = + mockServices.permissions.mock({ + authorize: jest.fn(), + authorizeConditional: jest.fn(), + }); - beforeAll(async () => { + const startPermissionDeniedTestServer = async () => { + permissionsMock.authorize.mockResolvedValue([ + { result: AuthorizeResult.DENY }, + ]); + const { server } = await startTestBackend({ + features: [ + mockServices.rootConfig.factory({ + data: { + kubernetes: { + serviceLocatorMethod: { type: 'multiTenant' }, + clusterLocatorMethods: [], + }, + }, + }), + permissionsMock.factory, + import('@backstage/plugin-kubernetes-backend'), + ], + }); + return server; + }; + + beforeEach(async () => { const objectsProviderMock = { getKubernetesObjectsByEntity: jest.fn().mockImplementation(args => { if (args.entity.metadata.name === 'inject500') { @@ -109,6 +140,8 @@ describe('resourcesRoutes', () => { }, }), import('@backstage/plugin-kubernetes-backend'), + import('@backstage/plugin-permission-backend'), + import('@backstage/plugin-permission-backend-module-allow-all-policy'), createBackendModule({ pluginId: 'kubernetes', moduleId: 'test-objects-provider', @@ -127,6 +160,10 @@ describe('resourcesRoutes', () => { app = server; }); + afterEach(() => { + app.stop(); + }); + describe('POST /resources/workloads/query', () => { // eslint-disable-next-line jest/expect-expect it('200 happy path', async () => { @@ -269,6 +306,13 @@ describe('resourcesRoutes', () => { response: { statusCode: 401 }, }); }); + it('403 when permission blocks endpoint', async () => { + app = await startPermissionDeniedTestServer(); + const response = await request(app).post( + '/api/kubernetes/resources/workloads/query', + ); + expect(response.status).toEqual(403); + }); // eslint-disable-next-line jest/expect-expect it('500 handle gracefully', async () => { await request(app) @@ -548,6 +592,13 @@ describe('resourcesRoutes', () => { response: { statusCode: 401 }, }); }); + it('403 when permission blocks endpoint', async () => { + app = await startPermissionDeniedTestServer(); + const response = await request(app).post( + '/api/kubernetes/resources/custom/query', + ); + expect(response.status).toEqual(403); + }); // eslint-disable-next-line jest/expect-expect it('500 handle gracefully', async () => { await request(app) diff --git a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts index 838e039d27..d500d3cb8b 100644 --- a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts +++ b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts @@ -23,6 +23,9 @@ import { InputError } from '@backstage/errors'; import express, { Request } from 'express'; import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { requirePermission } from '../auth/requirePermission'; +import { kubernetesResourcesReadPermission } from '@backstage/plugin-kubernetes-common'; export const addResourceRoutesToRouter = ( router: express.Router, @@ -30,6 +33,7 @@ export const addResourceRoutesToRouter = ( objectsProvider: KubernetesObjectsProvider, auth: AuthService, httpAuth: HttpAuthService, + permissionApi: PermissionEvaluator, ) => { const getEntityByReq = async (req: Request) => { const rawEntityRef = req.body.entityRef; @@ -62,6 +66,12 @@ export const addResourceRoutesToRouter = ( }; router.post('/resources/workloads/query', async (req, res) => { + await requirePermission( + permissionApi, + kubernetesResourcesReadPermission, + httpAuth, + req, + ); const entity = await getEntityByReq(req); const response = await objectsProvider.getKubernetesObjectsByEntity( { @@ -74,6 +84,12 @@ export const addResourceRoutesToRouter = ( }); router.post('/resources/custom/query', async (req, res) => { + await requirePermission( + permissionApi, + kubernetesResourcesReadPermission, + httpAuth, + req, + ); const entity = await getEntityByReq(req); if (!req.body.customResources) { diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index b38bf9121a..3046232bad 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -92,6 +92,19 @@ describe('API integration tests', () => { }); }, }); + const startPermissionDeniedTestServer = async () => { + permissionsMock.authorize.mockResolvedValue([ + { result: AuthorizeResult.DENY }, + ]); + const { server } = await startTestBackend({ + features: [ + minimalValidConfigService, + permissionsMock.factory, + import('@backstage/plugin-kubernetes-backend'), + ], + }); + return server; + }; beforeEach(async () => { jest.resetAllMocks(); @@ -305,6 +318,12 @@ describe('API integration tests', () => { items: [expect.objectContaining({ title: 'cluster-title' })], }); }); + + it('returns 403 response when permission blocks endpoint', async () => { + app = await startPermissionDeniedTestServer(); + const response = await request(app).get('/api/kubernetes/clusters'); + expect(response.status).toEqual(403); + }); }); describe('post /services/:serviceId', () => { @@ -504,6 +523,14 @@ describe('API integration tests', () => { }), ); }); + + it('returns 403 response when permission blocks endpoint', async () => { + app = await startPermissionDeniedTestServer(); + const response = await request(app).post( + '/api/kubernetes/services/test-service', + ); + expect(response.status).toEqual(403); + }); }); describe('/proxy', () => { @@ -571,18 +598,7 @@ metadata: }); it('returns 403 response when permission blocks endpoint', async () => { - permissionsMock.authorize.mockResolvedValue([ - { result: AuthorizeResult.DENY }, - ]); - - const { server } = await startTestBackend({ - features: [ - minimalValidConfigService, - permissionsMock.factory, - import('@backstage/plugin-kubernetes-backend'), - ], - }); - app = server; + app = await startPermissionDeniedTestServer(); const proxyEndpointRequest = request(app) .post('/api/kubernetes/proxy/api/v1/namespaces') @@ -779,6 +795,8 @@ metadata: expect(response.body).toMatchObject({ permissions: [ { type: 'basic', name: 'kubernetes.proxy', attributes: {} }, + { type: 'basic', name: 'kubernetes.resources.read', attributes: {} }, + { type: 'basic', name: 'kubernetes.clusters.read', attributes: {} }, ], rules: [], }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 6a4e9cc296..2d2c592f53 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -18,7 +18,9 @@ import { Config } from '@backstage/config'; import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, + kubernetesClustersReadPermission, kubernetesPermissions, + kubernetesResourcesReadPermission, } from '@backstage/plugin-kubernetes-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; @@ -73,6 +75,7 @@ import { } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { KubernetesProxy } from './KubernetesProxy'; +import { requirePermission } from '../auth/requirePermission'; /** * @deprecated Please migrate to the new backend system as this will be removed in the future. @@ -393,6 +396,12 @@ export class KubernetesBuilder { ); // @deprecated router.post('/services/:serviceId', async (req, res) => { + await requirePermission( + permissionApi, + kubernetesResourcesReadPermission, + httpAuth, + req, + ); const serviceId = req.params.serviceId; const requestBody: ObjectsByEntityRequest = req.body; try { @@ -413,6 +422,12 @@ export class KubernetesBuilder { }); router.get('/clusters', async (req, res) => { + await requirePermission( + permissionApi, + kubernetesClustersReadPermission, + httpAuth, + req, + ); const credentials = await httpAuth.credentials(req); const clusterDetails = await this.fetchClusterDetails(clusterSupplier, { credentials, @@ -447,6 +462,7 @@ export class KubernetesBuilder { objectsProvider, authService, httpAuth, + permissionApi, ); return router; diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 6ef4d85356..445850a681 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -10,9 +10,7 @@ ] }, "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" }, "keywords": [ "backstage", @@ -26,8 +24,23 @@ }, "license": "Apache-2.0", "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist" ], @@ -47,6 +60,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-kubernetes-react": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", "@kubernetes-models/apimachinery": "^2.0.0", "@kubernetes-models/base": "^5.0.0", "@material-ui/core": "^4.12.2", diff --git a/plugins/kubernetes-cluster/report-alpha.api.md b/plugins/kubernetes-cluster/report-alpha.api.md new file mode 100644 index 0000000000..13314d6c83 --- /dev/null +++ b/plugins/kubernetes-cluster/report-alpha.api.md @@ -0,0 +1,18 @@ +## API Report File for "@backstage/plugin-kubernetes-cluster" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; + +// @alpha (undocumented) +export const kubernetesClusterTranslationRef: TranslationRef< + 'kubernetes-cluster', + { + readonly 'kubernetesClusterContentPage.permissionAlert.message': "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' permission."; + readonly 'kubernetesClusterContentPage.permissionAlert.title': 'Permission required'; + } +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-app-api/src/alpha.ts b/plugins/kubernetes-cluster/src/alpha.ts similarity index 81% rename from packages/backend-app-api/src/alpha.ts rename to plugins/kubernetes-cluster/src/alpha.ts index a334214738..e49b7ac9c9 100644 --- a/packages/backend-app-api/src/alpha.ts +++ b/plugins/kubernetes-cluster/src/alpha.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2025 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. @@ -14,4 +14,4 @@ * limitations under the License. */ -export { featureDiscoveryServiceFactory } from './alpha/featureDiscoveryServiceFactory'; +export { kubernetesClusterTranslationRef } from './translation'; diff --git a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx index c46ac4c8e2..2a824d329a 100644 --- a/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx +++ b/plugins/kubernetes-cluster/src/components/KubernetesClusterContent/KubernetesClusterContent.tsx @@ -24,6 +24,10 @@ import { useKubernetesClusterError, } from '../KubernetesClusterErrorContext/KubernetesClusterErrorContext'; import { WarningPanel } from '@backstage/core-components'; +import { kubernetesClustersReadPermission } from '@backstage/plugin-kubernetes-common'; +import { RequirePermission } from '@backstage/plugin-permission-react'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesClusterTranslationRef } from '../../translation'; const ContentGrid = () => { const { error } = useKubernetesClusterError(); @@ -57,9 +61,21 @@ const ContentGrid = () => { * @public */ export const KubernetesClusterContent = () => { + const { t } = useTranslationRef(kubernetesClusterTranslationRef); + return ( - - - + + } + > + + + + ); }; diff --git a/plugins/kubernetes-cluster/src/translation.ts b/plugins/kubernetes-cluster/src/translation.ts new file mode 100644 index 0000000000..ba99c6744e --- /dev/null +++ b/plugins/kubernetes-cluster/src/translation.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const kubernetesClusterTranslationRef = createTranslationRef({ + id: 'kubernetes-cluster', + messages: { + kubernetesClusterContentPage: { + permissionAlert: { + title: 'Permission required', + message: + "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' permission.", + }, + }, + }, +}); diff --git a/plugins/kubernetes-common/report.api.md b/plugins/kubernetes-common/report.api.md index 8cdac7ed45..cc99e5063d 100644 --- a/plugins/kubernetes-common/report.api.md +++ b/plugins/kubernetes-common/report.api.md @@ -317,6 +317,9 @@ export interface JobsFetchResponse { type: 'jobs'; } +// @public +export const kubernetesClustersReadPermission: BasicPermission; + // @public (undocumented) export type KubernetesErrorTypes = | 'BAD_REQUEST' @@ -347,6 +350,9 @@ export interface KubernetesRequestBody { entity: Entity; } +// @public +export const kubernetesResourcesReadPermission: BasicPermission; + // @public (undocumented) export interface LimitRangeFetchResponse { // (undocumented) diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts index faaaaab92c..8378ac8199 100644 --- a/plugins/kubernetes-common/src/index.ts +++ b/plugins/kubernetes-common/src/index.ts @@ -25,6 +25,8 @@ export * from './catalog-entity-constants'; export * from './certificate-authority-constants'; export { kubernetesProxyPermission, + kubernetesClustersReadPermission, + kubernetesResourcesReadPermission, kubernetesPermissions, } from './permissions'; export * from './error-detection'; diff --git a/plugins/kubernetes-common/src/permissions.ts b/plugins/kubernetes-common/src/permissions.ts index d3ccbe8171..718251567c 100644 --- a/plugins/kubernetes-common/src/permissions.ts +++ b/plugins/kubernetes-common/src/permissions.ts @@ -24,8 +24,32 @@ export const kubernetesProxyPermission = createPermission({ attributes: {}, }); +/** This permission is used to check access to the /resources and /services/:serviceId endpoints + * @public + */ +export const kubernetesResourcesReadPermission = createPermission({ + name: 'kubernetes.resources.read', + attributes: { + action: 'read', + }, +}); + +/** This permission is used to check access to the /clusters endpoint + * @public + */ +export const kubernetesClustersReadPermission = createPermission({ + name: 'kubernetes.clusters.read', + attributes: { + action: 'read', + }, +}); + /** * List of all Kubernetes permissions. * @public */ -export const kubernetesPermissions = [kubernetesProxyPermission]; +export const kubernetesPermissions = [ + kubernetesProxyPermission, + kubernetesResourcesReadPermission, + kubernetesClustersReadPermission, +]; diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index 91b97cbfc5..e5581ac2ef 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -36,7 +36,8 @@ import fixture4 from '../src/__fixtures__/2-cronjobs.json'; import fixture5 from '../src/__fixtures__/1-rollouts.json'; import fixture6 from '../src/__fixtures__/3-ingresses.json'; import fixture7 from '../src/__fixtures__/2-statefulsets.json'; -import { TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; import { StructuredMetadataTable } from '@backstage/core-components'; const mockEntity: Entity = { @@ -149,7 +150,10 @@ createDevApp() title: 'Fixture 1', element: ( @@ -162,7 +166,10 @@ createDevApp() title: 'Fixture 2', element: ( @@ -175,7 +182,10 @@ createDevApp() title: 'Fixture 3', element: ( @@ -188,7 +198,10 @@ createDevApp() title: 'Fixture 4', element: ( @@ -201,7 +214,10 @@ createDevApp() title: 'Fixture 5', element: ( diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index cf99896620..c733cc00b3 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -66,6 +66,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-kubernetes-react": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", "@kubernetes-models/apimachinery": "^2.0.0", "@kubernetes-models/base": "^5.0.0", "@kubernetes/client-node": "1.0.0-rc7", diff --git a/plugins/kubernetes/report-alpha.api.md b/plugins/kubernetes/report-alpha.api.md index 4f779df258..19f0554291 100644 --- a/plugins/kubernetes/report-alpha.api.md +++ b/plugins/kubernetes/report-alpha.api.md @@ -13,6 +13,7 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { FrontendPlugin } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @public (undocumented) const _default: FrontendPlugin< @@ -162,5 +163,14 @@ const _default: FrontendPlugin< >; export default _default; +// @alpha (undocumented) +export const kubernetesTranslationRef: TranslationRef< + 'kubernetes', + { + readonly 'kubernetesContentPage.permissionAlert.message': "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' and 'kubernetes.resources.read' permission."; + readonly 'kubernetesContentPage.permissionAlert.title': 'Permission required'; + } +>; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/kubernetes/src/KubernetesContent.test.tsx b/plugins/kubernetes/src/KubernetesContent.test.tsx index 7c21c6f5a7..daeb184275 100644 --- a/plugins/kubernetes/src/KubernetesContent.test.tsx +++ b/plugins/kubernetes/src/KubernetesContent.test.tsx @@ -16,11 +16,16 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { + mockApis, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import { KubernetesContent } from './KubernetesContent'; import { useKubernetesObjects } from '@backstage/plugin-kubernetes-react'; import * as oneDeployment from './__fixtures__/1-deployments.json'; import * as twoDeployments from './__fixtures__/2-deployments.json'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; jest.mock('@backstage/plugin-kubernetes-react', () => ({ ...jest.requireActual('@backstage/plugin-kubernetes-react'), @@ -39,15 +44,17 @@ describe('KubernetesContent', () => { error: undefined, }); await renderInTestApp( - , + + + , ); expect(screen.getByText('Your Clusters')).toBeInTheDocument(); // TODO add a prompt for the user to configure their clusters @@ -81,15 +88,17 @@ describe('KubernetesContent', () => { error: undefined, }); await renderInTestApp( - , + + + , ); expect(screen.getByText('cluster-1')).toBeInTheDocument(); @@ -145,15 +154,17 @@ describe('KubernetesContent', () => { error: undefined, }); await renderInTestApp( - , + + + , ); expect(screen.getAllByText('Cluster')).toHaveLength(2); expect(screen.getByText('cluster-a')).toBeInTheDocument(); diff --git a/plugins/kubernetes/src/KubernetesContent.tsx b/plugins/kubernetes/src/KubernetesContent.tsx index acd1ecc521..f980e87f0b 100644 --- a/plugins/kubernetes/src/KubernetesContent.tsx +++ b/plugins/kubernetes/src/KubernetesContent.tsx @@ -35,6 +35,7 @@ import { Page, Progress, } from '@backstage/core-components'; +import { RequireKubernetesPermissions } from './RequireKubernetesPermissions'; type KubernetesContentProps = { entity: Entity; @@ -62,89 +63,93 @@ export const KubernetesContent = ({ : new Map(); return ( - - - - {kubernetesObjects === undefined && error === undefined && ( - - )} + + + + + {kubernetesObjects === undefined && error === undefined && ( + + )} - {/* errors retrieved from the kubernetes clusters */} - {clustersWithErrors.length > 0 && ( - - - + {/* errors retrieved from the kubernetes clusters */} + {clustersWithErrors.length > 0 && ( + + + + - - )} + )} - {/* other errors */} - {error !== undefined && ( - - - + {/* other errors */} + {error !== undefined && ( + + + + - - )} + )} - {kubernetesObjects && ( - - - - - - Your Clusters - - - {kubernetesObjects?.items.length <= 0 && ( - - - - - - )} - {kubernetesObjects?.items.length > 0 && - kubernetesObjects?.items.map((item, i) => { - const podsWithErrors = new Set( - detectedErrors - .get(item.cluster.name) - ?.filter(de => de.sourceRef.kind === 'Pod') - .map(de => de.sourceRef.name), - ); - - return ( - - + + + + + Your Clusters + + + {kubernetesObjects?.items.length <= 0 && ( + + + - ); - })} + + )} + {kubernetesObjects?.items.length > 0 && + kubernetesObjects?.items.map((item, i) => { + const podsWithErrors = new Set( + detectedErrors + .get(item.cluster.name) + ?.filter(de => de.sourceRef.kind === 'Pod') + .map(de => de.sourceRef.name), + ); + + return ( + + + + ); + })} + - - )} - - - + )} + + + + ); }; diff --git a/plugins/kubernetes/src/RequireKubernetesPermissions.tsx b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx new file mode 100644 index 0000000000..efc83e8ef8 --- /dev/null +++ b/plugins/kubernetes/src/RequireKubernetesPermissions.tsx @@ -0,0 +1,61 @@ +/* + * Copyright 2025 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, { ReactNode } from 'react'; +import { + kubernetesClustersReadPermission, + kubernetesResourcesReadPermission, +} from '@backstage/plugin-kubernetes-common'; +import { usePermission } from '@backstage/plugin-permission-react'; +import { WarningPanel } from '@backstage/core-components'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesTranslationRef } from './alpha/translation'; + +export type RequireKubernetesPermissionProps = { + children: ReactNode; +}; + +export function RequireKubernetesPermissions( + props: RequireKubernetesPermissionProps, +): JSX.Element | null { + const kubernetesClustersPermissionResult = usePermission({ + permission: kubernetesClustersReadPermission, + }); + const kubernetesResourcesPermissionResult = usePermission({ + permission: kubernetesResourcesReadPermission, + }); + const { t } = useTranslationRef(kubernetesTranslationRef); + + if ( + kubernetesClustersPermissionResult.loading || + kubernetesResourcesPermissionResult.loading + ) { + return null; + } + + if ( + kubernetesClustersPermissionResult.allowed && + kubernetesResourcesPermissionResult.allowed + ) { + return <>{props.children}; + } + + return ( + + ); +} diff --git a/plugins/kubernetes/src/alpha/index.ts b/plugins/kubernetes/src/alpha/index.ts index 2f137f09ee..23888f968f 100644 --- a/plugins/kubernetes/src/alpha/index.ts +++ b/plugins/kubernetes/src/alpha/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +export { kubernetesTranslationRef } from './translation'; export { default } from './plugin'; diff --git a/plugins/kubernetes/src/alpha/translation.ts b/plugins/kubernetes/src/alpha/translation.ts new file mode 100644 index 0000000000..7f9704af84 --- /dev/null +++ b/plugins/kubernetes/src/alpha/translation.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const kubernetesTranslationRef = createTranslationRef({ + id: 'kubernetes', + messages: { + kubernetesContentPage: { + permissionAlert: { + title: 'Permission required', + message: + "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' and 'kubernetes.resources.read' permission.", + }, + }, + }, +}); diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 782846dec7..82d7a66e92 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -481,6 +481,17 @@ export function getOctokitOptions(options: { owner?: string; repo?: string; token?: string; + host: string; + owner?: string; + repo?: string; +}): Promise; + +// @public @deprecated +export function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; + token?: string; + repoUrl: string; }): Promise; // @public diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index 4c04087670..6c47a2dade 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -227,6 +227,10 @@ export function createPublishGithubAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, @@ -237,10 +241,6 @@ export function createPublishGithubAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } - const newRepo = await createGithubRepoWithCollaboratorsAndTopics( client, repo, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index 5d5366a7b1..6de447541b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -117,6 +117,10 @@ export function createGithubBranchProtectionAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, @@ -126,10 +130,6 @@ export function createGithubBranchProtectionAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const repository = await client.rest.repos.get({ owner: owner, repo: repo, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts index 6a7a2f86c6..a91ac8ba5e 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts @@ -109,6 +109,10 @@ export function createGithubDeployKeyAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, @@ -117,10 +121,6 @@ export function createGithubDeployKeyAction(options: { repo, }); - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const client = new Octokit(octokitOptions); await client.rest.repos.createDeployKey({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index 5d3817ee6d..49bd96db9d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -177,6 +177,10 @@ export function createGithubEnvironmentAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, @@ -185,10 +189,6 @@ export function createGithubEnvironmentAction(options: { repo, }); - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const client = new Octokit(octokitOptions); const repository = await client.rest.repos.get({ owner: owner, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts index 54bcfbcc43..9212f77338 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts @@ -94,6 +94,10 @@ export function createGithubPagesEnableAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, @@ -104,10 +108,6 @@ export function createGithubPagesEnableAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } - ctx.logger.info( `Attempting to enable GitHub Pages for ${owner}/${repo} with "${buildType}" build type, on source branch "${sourceBranch}" and source path "${sourcePath}"`, ); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index b103dffb15..d32f7447a6 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -182,6 +182,10 @@ export function createGithubRepoCreateAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, @@ -192,10 +196,6 @@ export function createGithubRepoCreateAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } - const newRepo = await createGithubRepoWithCollaboratorsAndTopics( client, repo, diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts index 575ad8c6ed..c25ca99f5f 100644 --- a/plugins/scaffolder-backend-module-github/src/util.ts +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -20,32 +20,58 @@ import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; +import { parseRepoUrl } from '@backstage/plugin-scaffolder-node'; import { OctokitOptions } from '@octokit/core/dist-types/types'; const DEFAULT_TIMEOUT_MS = 60_000; /** - * Helper for generating octokit configuration options for given repoUrl. + * Helper for generating octokit configuration options. * If no token is provided, it will attempt to get a token from the credentials provider. * @public */ export async function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; credentialsProvider?: GithubCredentialsProvider; + token?: string; host: string; owner?: string; repo?: string; +}): Promise; + +/** + * Helper for generating octokit configuration options for given repoUrl. + * If no token is provided, it will attempt to get a token from the credentials provider. + * @public + * @deprecated Use options `host`, `owner` and `repo` instead of `repoUrl`. + */ +export async function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; token?: string; + repoUrl: string; +}): Promise; + +export async function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; + token?: string; + host?: string; + owner?: string; + repo?: string; + repoUrl?: string; }): Promise { - const { integrations, credentialsProvider, host, owner, repo, token } = - options; + const { integrations, credentialsProvider, token, repoUrl } = options; + const { host, owner, repo } = repoUrl + ? parseRepoUrl(repoUrl, integrations) + : options; const requestOptions = { // set timeout to 60 seconds timeout: DEFAULT_TIMEOUT_MS, }; - const integrationConfig = integrations.github.byHost(host)?.config; + const integrationConfig = integrations.github.byHost(host!)?.config; if (!integrationConfig) { throw new InputError(`No integration for host ${host}`); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts index 869bda30ec..d577adc896 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts @@ -24,7 +24,9 @@ import yaml from 'yaml'; const mockGitlabClient = { Migrations: { - create: jest.fn(), + create: jest.fn(() => ({ + id: 0, + })), }, }; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts index 8e3bc6560d..e1bf9af138 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts @@ -22,7 +22,9 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; const mockGitlabClient = { Migrations: { - create: jest.fn(), + create: jest.fn(() => ({ + id: 0, + })), }, }; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts index 659215c1c0..8f6e3ed2cc 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts @@ -89,6 +89,10 @@ export const createGitlabProjectMigrateAction = (options: { title: 'URL to the newly imported repo', type: 'string', }, + migrationId: { + title: 'Id of the migration that imports the project', + type: 'number', + }, }, }, }, @@ -135,17 +139,21 @@ export const createGitlabProjectMigrateAction = (options: { }; try { - await api.Migrations.create(sourceConfig, migrationEntity); + const { id: migrationId } = await api.Migrations.create( + sourceConfig, + migrationEntity, + ); + + ctx.output( + 'importedRepoUrl', + `${destinationHost}/${destinationNamespace}/${destinationSlug}`, + ); + ctx.output('migrationId', migrationId); } catch (e: any) { throw new InputError( `Failed to transfer repo ${sourceFullPath}. Make sure that ${sourceFullPath} exists in ${sourceUrl}, and token has enough rights.\nError: ${e}`, ); } - - ctx.output( - 'importedRepoUrl', - `${destinationHost}/${destinationNamespace}/${destinationSlug}`, - ); }, }); }; diff --git a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts index 4fa906686c..a2e2f18ab2 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts @@ -79,8 +79,8 @@ describe('handleAutocompleteRequest', () => { mockClient.Users.showCurrentUser.mockResolvedValue({ id: 3 }); mockClient.Groups.allProjects.mockResolvedValue([ - { name: 'repo1', id: 1 }, - { name: 'repo2', id: 2 }, + { name: 'Repo 1', path: 'repo-1' }, + { name: 'Repo 2', path: 'repo-2' }, ]); const result = await handleAutocompleteRequest({ @@ -91,8 +91,8 @@ describe('handleAutocompleteRequest', () => { expect(result).toEqual({ results: [ - { title: 'repo1', id: '1' }, - { title: 'repo2', id: '2' }, + { title: 'Repo 1', id: 'repo-1' }, + { title: 'Repo 2', id: 'repo-2' }, ], }); }); @@ -104,8 +104,8 @@ describe('handleAutocompleteRequest', () => { mockClient.Users.showCurrentUser.mockResolvedValue({ id: 1 }); mockClient.Users.allProjects.mockResolvedValue([ - { name: 'repo1', id: 1 }, - { name: 'repo2', id: 2 }, + { name: 'Repo 1', path: 'repo-1' }, + { name: 'Repo 2', path: 'repo-2' }, ]); const result = await handleAutocompleteRequest({ @@ -116,8 +116,8 @@ describe('handleAutocompleteRequest', () => { expect(result).toEqual({ results: [ - { title: 'repo1', id: '1' }, - { title: 'repo2', id: '2' }, + { title: 'Repo 1', id: 'repo-1' }, + { title: 'Repo 2', id: 'repo-2' }, ], }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts index 6e9a7f79c4..4eddd844e5 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts @@ -97,7 +97,7 @@ export function createHandleAutocompleteRequest(options: { return { results: response.map(project => ({ title: project.name.trim(), - id: project.id.toString(), + id: project.path, })), }; } diff --git a/plugins/scaffolder-backend/config.d.ts b/plugins/scaffolder-backend/config.d.ts index 90040d68f1..a391f97264 100644 --- a/plugins/scaffolder-backend/config.d.ts +++ b/plugins/scaffolder-backend/config.d.ts @@ -41,6 +41,11 @@ export interface Config { */ concurrentTasksLimit?: number; + /** + * Tries to wait for tasks to finish during SIGTERM before shutting down the TaskWorker. + */ + EXPERIMENTAL_gracefulShutdown?: boolean; + /** * Sets the tasks recoverability on system start up. * diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index f21bcd76f7..8fe17d26f8 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -9,14 +9,27 @@ import { ActionContext as ActionContext_2 } from '@backstage/plugin-scaffolder-n import { AuditorService } from '@backstage/backend-plugin-api'; import { AuthService } from '@backstage/backend-plugin-api'; import { AutocompleteHandler } from '@backstage/plugin-scaffolder-node/alpha'; -import * as azure from '@backstage/plugin-scaffolder-backend-module-azure'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; -import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucket'; -import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; -import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; +import { createGithubActionsDispatchAction as createGithubActionsDispatchAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubDeployKeyAction as createGithubDeployKeyAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubEnvironmentAction as createGithubEnvironmentAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubIssuesLabelAction as createGithubIssuesLabelAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { CreateGithubPullRequestActionOptions as CreateGithubPullRequestActionOptions_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubRepoCreateAction as createGithubRepoCreateAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubRepoPushAction as createGithubRepoPushAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubWebhookAction as createGithubWebhookAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createPublishAzureAction as createPublishAzureAction_2 } from '@backstage/plugin-scaffolder-backend-module-azure'; +import { createPublishBitbucketAction as createPublishBitbucketAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket'; +import { createPublishBitbucketCloudAction as createPublishBitbucketCloudAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; +import { createPublishBitbucketServerAction as createPublishBitbucketServerAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; +import { createPublishBitbucketServerPullRequestAction as createPublishBitbucketServerPullRequestAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; +import { createPublishGerritAction as createPublishGerritAction_2 } from '@backstage/plugin-scaffolder-backend-module-gerrit'; +import { createPublishGerritReviewAction as createPublishGerritReviewAction_2 } from '@backstage/plugin-scaffolder-backend-module-gerrit'; +import { createPublishGithubAction as createPublishGithubAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createPublishGitlabAction as createPublishGitlabAction_2 } from '@backstage/plugin-scaffolder-backend-module-gitlab'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import { Duration } from 'luxon'; @@ -25,9 +38,6 @@ import { executeShellCommand as executeShellCommand_2 } from '@backstage/plugin- import { ExecuteShellCommandOptions } from '@backstage/plugin-scaffolder-node'; import express from 'express'; import { fetchContents as fetchContents_2 } from '@backstage/plugin-scaffolder-node'; -import * as gerrit from '@backstage/plugin-scaffolder-backend-module-gerrit'; -import * as github from '@backstage/plugin-scaffolder-backend-module-github'; -import * as gitlab from '@backstage/plugin-scaffolder-backend-module-gitlab'; import { HttpAuthService } from '@backstage/backend-plugin-api'; import { HumanDuration } from '@backstage/types'; import { IdentityApi } from '@backstage/plugin-auth-node'; @@ -265,57 +275,57 @@ export const createFilesystemRenameAction: () => TemplateAction_2< >; // @public @deprecated (undocumented) -export const createGithubActionsDispatchAction: typeof github.createGithubActionsDispatchAction; +export const createGithubActionsDispatchAction: typeof createGithubActionsDispatchAction_2; // @public @deprecated (undocumented) -export const createGithubDeployKeyAction: typeof github.createGithubDeployKeyAction; +export const createGithubDeployKeyAction: typeof createGithubDeployKeyAction_2; // @public @deprecated (undocumented) -export const createGithubEnvironmentAction: typeof github.createGithubEnvironmentAction; +export const createGithubEnvironmentAction: typeof createGithubEnvironmentAction_2; // @public @deprecated (undocumented) -export const createGithubIssuesLabelAction: typeof github.createGithubIssuesLabelAction; +export const createGithubIssuesLabelAction: typeof createGithubIssuesLabelAction_2; // @public @deprecated (undocumented) export type CreateGithubPullRequestActionOptions = - github.CreateGithubPullRequestActionOptions; + CreateGithubPullRequestActionOptions_2; // @public @deprecated (undocumented) -export const createGithubRepoCreateAction: typeof github.createGithubRepoCreateAction; +export const createGithubRepoCreateAction: typeof createGithubRepoCreateAction_2; // @public @deprecated (undocumented) -export const createGithubRepoPushAction: typeof github.createGithubRepoPushAction; +export const createGithubRepoPushAction: typeof createGithubRepoPushAction_2; // @public @deprecated (undocumented) -export const createGithubWebhookAction: typeof github.createGithubWebhookAction; +export const createGithubWebhookAction: typeof createGithubWebhookAction_2; // @public @deprecated (undocumented) -export const createPublishAzureAction: typeof azure.createPublishAzureAction; +export const createPublishAzureAction: typeof createPublishAzureAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketAction: typeof bitbucket.createPublishBitbucketAction; +export const createPublishBitbucketAction: typeof createPublishBitbucketAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketCloudAction: typeof bitbucketCloud.createPublishBitbucketCloudAction; +export const createPublishBitbucketCloudAction: typeof createPublishBitbucketCloudAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketServerAction: typeof bitbucketServer.createPublishBitbucketServerAction; +export const createPublishBitbucketServerAction: typeof createPublishBitbucketServerAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketServerPullRequestAction: typeof bitbucketServer.createPublishBitbucketServerPullRequestAction; +export const createPublishBitbucketServerPullRequestAction: typeof createPublishBitbucketServerPullRequestAction_2; // @public @deprecated (undocumented) -export const createPublishGerritAction: typeof gerrit.createPublishGerritAction; +export const createPublishGerritAction: typeof createPublishGerritAction_2; // @public @deprecated (undocumented) -export const createPublishGerritReviewAction: typeof gerrit.createPublishGerritReviewAction; +export const createPublishGerritReviewAction: typeof createPublishGerritReviewAction_2; // @public @deprecated (undocumented) -export const createPublishGithubAction: typeof github.createPublishGithubAction; +export const createPublishGithubAction: typeof createPublishGithubAction_2; // @public @deprecated (undocumented) export const createPublishGithubPullRequestAction: ( - options: github.CreateGithubPullRequestActionOptions, + options: CreateGithubPullRequestActionOptions_2, ) => TemplateAction_2< { title: string; @@ -340,7 +350,7 @@ export const createPublishGithubPullRequestAction: ( >; // @public @deprecated (undocumented) -export const createPublishGitlabAction: typeof gitlab.createPublishGitlabAction; +export const createPublishGitlabAction: typeof createPublishGitlabAction_2; // @public @deprecated (undocumented) export const createPublishGitlabMergeRequestAction: (options: { @@ -414,6 +424,7 @@ export type CreateWorkerOptions = { concurrentTasksLimit?: number; additionalTemplateGlobals?: Record; permissions?: PermissionEvaluator; + gracefulShutdown?: boolean; }; // @public @@ -841,7 +852,7 @@ export class TaskWorker { // (undocumented) start(): void; // (undocumented) - stop(): void; + stop(): Promise; } // @public @deprecated (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts b/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts index 5d037b44b6..324e9b43e1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts @@ -13,134 +13,151 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as github from '@backstage/plugin-scaffolder-backend-module-github'; -import * as gitlab from '@backstage/plugin-scaffolder-backend-module-gitlab'; -import * as azure from '@backstage/plugin-scaffolder-backend-module-azure'; -import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucket'; -import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; -import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; -import * as gerrit from '@backstage/plugin-scaffolder-backend-module-gerrit'; +import { + createGithubActionsDispatchAction as githubActionsDispatch, + createGithubDeployKeyAction as githubDeployKey, + createGithubEnvironmentAction as githubEnvironment, + createGithubIssuesLabelAction as githubIssuesLabel, + CreateGithubPullRequestActionOptions as GithubPullRequestActionOptions, + createGithubRepoCreateAction as githubRepoCreate, + createGithubRepoPushAction as githubRepoPush, + createGithubWebhookAction as githubWebhook, + createPublishGithubAction as publishGithub, + createPublishGithubPullRequestAction as publishGithubPullRequest, +} from '@backstage/plugin-scaffolder-backend-module-github'; + +import { + createPublishGitlabAction as publishGitlab, + createPublishGitlabMergeRequestAction as publishGitlabMergeRequest, +} from '@backstage/plugin-scaffolder-backend-module-gitlab'; + +import { createPublishAzureAction as publishAzure } from '@backstage/plugin-scaffolder-backend-module-azure'; + +import { createPublishBitbucketAction as publishBitbucket } from '@backstage/plugin-scaffolder-backend-module-bitbucket'; + +import { createPublishBitbucketCloudAction as publishBitbucketCloud } from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; + +import { + createPublishBitbucketServerAction as publishBitbucketServer, + createPublishBitbucketServerPullRequestAction as publishBitbucketServerPullRequest, +} from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; + +import { + createPublishGerritAction as publishGerrit, + createPublishGerritReviewAction as publishGerritReview, +} from '@backstage/plugin-scaffolder-backend-module-gerrit'; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubActionsDispatchAction = - github.createGithubActionsDispatchAction; +export const createGithubActionsDispatchAction = githubActionsDispatch; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubDeployKeyAction = github.createGithubDeployKeyAction; +export const createGithubDeployKeyAction = githubDeployKey; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubEnvironmentAction = - github.createGithubEnvironmentAction; +export const createGithubEnvironmentAction = githubEnvironment; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubIssuesLabelAction = - github.createGithubIssuesLabelAction; +export const createGithubIssuesLabelAction = githubIssuesLabel; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export type CreateGithubPullRequestActionOptions = - github.CreateGithubPullRequestActionOptions; + GithubPullRequestActionOptions; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubRepoCreateAction = github.createGithubRepoCreateAction; +export const createGithubRepoCreateAction = githubRepoCreate; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubRepoPushAction = github.createGithubRepoPushAction; +export const createGithubRepoPushAction = githubRepoPush; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubWebhookAction = github.createGithubWebhookAction; +export const createGithubWebhookAction = githubWebhook; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createPublishGithubAction = github.createPublishGithubAction; +export const createPublishGithubAction = publishGithub; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createPublishGithubPullRequestAction = - github.createPublishGithubPullRequestAction; +export const createPublishGithubPullRequestAction = publishGithubPullRequest; /** * @public @deprecated use "createPublishBitbucketCloudAction" from \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud or "createPublishBitbucketServerAction" from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead */ -export const createPublishBitbucketAction = - bitbucket.createPublishBitbucketAction; +export const createPublishBitbucketAction = publishBitbucket; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead */ -export const createPublishBitbucketCloudAction = - bitbucketCloud.createPublishBitbucketCloudAction; +export const createPublishBitbucketCloudAction = publishBitbucketCloud; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ -export const createPublishBitbucketServerAction = - bitbucketServer.createPublishBitbucketServerAction; +export const createPublishBitbucketServerAction = publishBitbucketServer; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerPullRequestAction = - bitbucketServer.createPublishBitbucketServerPullRequestAction; + publishBitbucketServerPullRequest; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-azure` instead */ -export const createPublishAzureAction = azure.createPublishAzureAction; +export const createPublishAzureAction = publishAzure; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead */ -export const createPublishGerritAction = gerrit.createPublishGerritAction; +export const createPublishGerritAction = publishGerrit; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead */ -export const createPublishGerritReviewAction = - gerrit.createPublishGerritReviewAction; +export const createPublishGerritReviewAction = publishGerritReview; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead */ -export const createPublishGitlabAction = gitlab.createPublishGitlabAction; +export const createPublishGitlabAction = publishGitlab; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead */ -export const createPublishGitlabMergeRequestAction = - gitlab.createPublishGitlabMergeRequestAction; +export const createPublishGitlabMergeRequestAction = publishGitlabMergeRequest; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 6c77ef3413..4441a2f0ca 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -29,6 +29,7 @@ import { Logger } from 'winston'; import { TemplateActionRegistry } from '../actions'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { WorkflowRunner } from './types'; +import { setTimeout } from 'timers/promises'; /** * TaskWorkerOptions @@ -44,6 +45,7 @@ export type TaskWorkerOptions = { permissions?: PermissionEvaluator; logger?: Logger; auditor?: AuditorService; + gracefulShutdown?: boolean; }; /** @@ -74,6 +76,7 @@ export type CreateWorkerOptions = { concurrentTasksLimit?: number; additionalTemplateGlobals?: Record; permissions?: PermissionEvaluator; + gracefulShutdown?: boolean; }; /** @@ -108,6 +111,7 @@ export class TaskWorker { concurrentTasksLimit = 10, // from 1 to Infinity additionalTemplateGlobals, permissions, + gracefulShutdown, } = options; const workflowRunner = new NunjucksWorkflowRunner({ @@ -127,6 +131,7 @@ export class TaskWorker { concurrentTasksLimit, permissions, auditor, + gracefulShutdown, }); } @@ -141,7 +146,7 @@ export class TaskWorker { start() { (async () => { while (!this.stopWorkers) { - await new Promise(resolve => setTimeout(resolve, 10000)); + await setTimeout(10000); await this.recoverTasks(); } })(); @@ -156,8 +161,13 @@ export class TaskWorker { })(); } - stop() { + async stop() { this.stopWorkers = true; + if (this.options?.gracefulShutdown) { + while (this.taskQueue.size > 0) { + await setTimeout(1000); + } + } } protected onReadyToClaimTask(): Promise { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index a12e43615e..c39ee29a1e 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -366,6 +366,10 @@ export async function createRouter( const workers: TaskWorker[] = []; if (concurrentTasksLimit !== 0) { + const gracefulShutdown = config.getOptionalBoolean( + 'scaffolder.EXPERIMENTAL_gracefulShutdown', + ); + for (let i = 0; i < (taskWorkers || 1); i++) { const worker = await TaskWorker.create({ taskBroker, @@ -378,6 +382,7 @@ export async function createRouter( additionalTemplateGlobals, concurrentTasksLimit, permissions, + gracefulShutdown, }); workers.push(worker); } @@ -399,8 +404,8 @@ export async function createRouter( const launchWorkers = () => workers.forEach(worker => worker.start()); - const shutdownWorkers = () => { - workers.forEach(worker => worker.stop()); + const shutdownWorkers = async () => { + await Promise.allSettled(workers.map(worker => worker.stop())); }; if (options.lifecycle) { diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx index 7ec3b0867c..cea00b15b2 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx @@ -78,4 +78,41 @@ describe('', () => { expect(queryByTestId('output-box')).toBeNull(); expect(queryByTestId('text-output-box')).toBeNull(); }); + it('should not render anything when only a single text output is defined', async () => { + // This is the default case when no output field is present in the template + const output = { + text: [ + { title: 'Text 1', content: 'Hello, **world**!', showButton: false }, + ], + }; + const { queryByTestId } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + // Ensure that nothing renders from this component + expect(queryByTestId('output-box')).toBeNull(); + }); + it('should not render text output buttons if there is only one output', async () => { + const output = { + links: [{ title: 'Link 1', url: 'https://backstage.io/' }], + text: [ + { title: 'Text 1', content: 'Hello, **world**!', showButton: false }, + ], + }; + const { queryByTestId } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(queryByTestId('text-outputs')).toBeNull(); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index 70f3b1fa59..40abad376b 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -56,7 +56,8 @@ export const DefaultTemplateOutputs = (props: { return null; } - const emptyOutput = Object.keys(output).length === 0; + const displayTextButtons = (output.text || []).length > 1; + const emptyOutput = (output.links || []).length === 0 && !displayTextButtons; return ( <> @@ -70,11 +71,14 @@ export const DefaultTemplateOutputs = (props: { gridGap={16} flexWrap="wrap" > - + {displayTextButtons && ( + + )} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 7a985265d5..3c6eba1695 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -248,8 +248,7 @@ export const RepoUrlPicker = ( onChange={repo => setState(prevState => ({ ...prevState, - repoName: repo.name, - id: repo.id || '', + repoName: repo.id || repo.name, })) } rawErrors={rawErrors} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts index d37e764eae..74d45fa401 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts @@ -25,7 +25,6 @@ export interface RepoUrlPickerState { organization?: string; workspace?: string; project?: string; - id?: string; availableRepos?: AvailableRepositories[]; } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts index ce3c938e31..c90b357cf1 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts @@ -53,10 +53,9 @@ describe('utils', () => { organization: 'organization', workspace: 'workspace', project: 'backstage', - id: '1234', }), ).toBe( - 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage&id=1234', + 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage', ); }); }); @@ -65,7 +64,7 @@ describe('utils', () => { it('should parse a complete string', () => { expect( parseRepoPickerUrl( - 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage&id=1234', + 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage', ), ).toEqual({ host: 'github.com', @@ -74,7 +73,6 @@ describe('utils', () => { organization: 'organization', workspace: 'workspace', project: 'backstage', - id: '1234', }); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts index 145a3e256f..408b87413b 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts @@ -37,9 +37,6 @@ export function serializeRepoPickerUrl(data: RepoUrlPickerState) { if (data.project) { params.set('project', data.project); } - if (data.id) { - params.set('id', data.id); - } return `${data.host}?${params.toString()}`; } @@ -53,7 +50,6 @@ export function parseRepoPickerUrl( let organization = ''; let workspace = ''; let project = ''; - let id = ''; try { if (url) { @@ -64,10 +60,9 @@ export function parseRepoPickerUrl( organization = parsed.searchParams.get('organization') || ''; workspace = parsed.searchParams.get('workspace') || ''; project = parsed.searchParams.get('project') || ''; - id = parsed.searchParams.get('id') || ''; } } catch { /* ok */ } - return { host, owner, repoName, organization, workspace, project, id }; + return { host, owner, repoName, organization, workspace, project }; } diff --git a/yarn.lock b/yarn.lock index 8b6c025c23..e2c6da4e91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4333,6 +4333,7 @@ __metadata: "@backstage/errors": ^1.2.4 "@backstage/theme": ^0.6.0 "@backstage/version-bridge": ^1.0.10 + "@dagrejs/dagre": ^1.1.4 "@date-io/core": ^1.3.13 "@material-table/core": ^3.1.0 "@material-ui/core": ^4.12.2 @@ -4345,7 +4346,6 @@ __metadata: d3-selection: ^3.0.0 d3-shape: ^3.0.0 d3-zoom: ^3.0.0 - dagre: ^0.8.5 linkify-react: 4.1.3 linkifyjs: 4.1.3 lodash: ^4.17.21 @@ -4442,6 +4442,7 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/version-bridge": "workspace:^" + "@dagrejs/dagre": ^1.1.4 "@date-io/core": ^1.3.13 "@material-table/core": ^3.1.0 "@material-ui/core": ^4.12.2 @@ -4457,7 +4458,6 @@ __metadata: "@types/d3-selection": ^3.0.1 "@types/d3-shape": ^3.0.1 "@types/d3-zoom": ^3.0.1 - "@types/dagre": ^0.7.44 "@types/google-protobuf": ^3.7.2 "@types/react": ^18.0.0 "@types/react-helmet": ^6.1.0 @@ -4472,7 +4472,6 @@ __metadata: d3-selection: ^3.0.0 d3-shape: ^3.0.0 d3-zoom: ^3.0.0 - dagre: ^0.8.5 history: ^5.0.0 js-yaml: ^4.1.0 linkify-react: 4.1.3 @@ -5191,6 +5190,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" express: ^4.18.2 passport: ^0.7.0 passport-atlassian-oauth2: ^2.1.0 @@ -5208,6 +5208,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/passport-auth0": ^1.0.5 "@types/passport-oauth2": ^1.4.15 express: ^4.17.1 @@ -5228,6 +5229,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" express: ^4.18.2 jose: ^5.0.0 msw: ^2.0.8 @@ -5263,6 +5265,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" express: ^4.18.2 passport: ^0.7.0 passport-bitbucket-oauth2: ^0.1.2 @@ -5280,6 +5283,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/passport-oauth2": ^1.4.15 passport: ^0.7.0 passport-oauth2: ^1.6.1 @@ -5333,6 +5337,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" passport-github2: ^0.1.12 supertest: ^7.0.0 languageName: unknown @@ -5348,6 +5353,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" express: ^4.18.2 passport: ^0.7.0 passport-gitlab2: ^5.0.0 @@ -5364,6 +5370,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/passport-google-oauth20": ^2.0.3 google-auth-library: ^9.0.0 passport-google-oauth20: ^2.0.0 @@ -5398,6 +5405,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/passport-microsoft": ^1.0.0 express: ^4.18.2 jose: ^5.0.0 @@ -5417,6 +5425,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" passport: ^0.7.0 passport-oauth2: ^1.6.1 supertest: ^7.0.0 @@ -5447,6 +5456,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" cookie-parser: ^1.4.6 express: ^4.18.2 express-promise-router: ^4.1.1 @@ -5469,6 +5479,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@davidzemon/passport-okta-oauth": ^0.0.5 express: ^4.18.2 passport: ^0.7.0 @@ -5486,6 +5497,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" express: ^4.18.2 passport: ^0.7.0 passport-onelogin-oauth: ^0.0.1 @@ -5504,6 +5516,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" cookie-parser: ^1.4.6 express: ^4.18.2 express-session: ^1.17.3 @@ -5529,6 +5542,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" "@types/passport-oauth2": ^1.4.15 jose: ^5.0.0 msw: ^2.0.8 @@ -6860,6 +6874,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/plugin-kubernetes-react": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@kubernetes-models/apimachinery": ^2.0.0 "@kubernetes-models/base": ^5.0.0 @@ -6988,6 +7003,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/plugin-kubernetes-react": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@kubernetes-models/apimachinery": ^2.0.0 "@kubernetes-models/base": ^5.0.0 @@ -9375,6 +9391,22 @@ __metadata: languageName: node linkType: hard +"@dagrejs/dagre@npm:^1.1.4": + version: 1.1.4 + resolution: "@dagrejs/dagre@npm:1.1.4" + dependencies: + "@dagrejs/graphlib": 2.2.4 + checksum: 39d29a0ea34d1782fee012a87b0395e1959d81f2f8c9cdf4b236e2b703f88524ae2de3d7c4eccce85758c8d26966e1ee147d1075201af059443f6da967e5bfd2 + languageName: node + linkType: hard + +"@dagrejs/graphlib@npm:2.2.4": + version: 2.2.4 + resolution: "@dagrejs/graphlib@npm:2.2.4" + checksum: 02d2d8df07fd6234bfc82f03f0aebd718f1ffc2ba6579ef67a26e423d9ad25a437256d3342e6342e29ae8a5c5e448a07440d6e218c6e77e390bc57ee73b43d2d + languageName: node + linkType: hard + "@date-io/core@npm:1.x, @date-io/core@npm:^1.3.13": version: 1.3.13 resolution: "@date-io/core@npm:1.3.13" @@ -19548,13 +19580,6 @@ __metadata: languageName: node linkType: hard -"@types/dagre@npm:^0.7.44": - version: 0.7.52 - resolution: "@types/dagre@npm:0.7.52" - checksum: 89e046e73f9f83855fcecc0f79838e0e3e0e42d88b6cc42bb573249364a606909ff7ded5b6a0377246eb0648047b330b5003c8dd975358de3135635ddae0f589 - languageName: node - linkType: hard - "@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.12, @types/debug@npm:^4.1.7": version: 4.1.12 resolution: "@types/debug@npm:4.1.12"