diff --git a/.changeset/angry-ducks-shake.md b/.changeset/angry-ducks-shake.md new file mode 100644 index 0000000000..58975123b4 --- /dev/null +++ b/.changeset/angry-ducks-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Allow specifying custom Docker registry for database tests diff --git a/.changeset/big-days-press.md b/.changeset/big-days-press.md new file mode 100644 index 0000000000..7dec7d6288 --- /dev/null +++ b/.changeset/big-days-press.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Expose both types of scaffolder permissions and rules through the metadata endpoint. + +The metadata endpoint now correctly exposes both types of scaffolder permissions and rules (for both the template and action resource types) through the metadata endpoint. diff --git a/.changeset/blue-eyes-run.md b/.changeset/blue-eyes-run.md new file mode 100644 index 0000000000..1b96e01f1c --- /dev/null +++ b/.changeset/blue-eyes-run.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-devtools': minor +'@backstage/plugin-devtools-backend': minor +'@backstage/plugin-devtools-common': minor +--- + +Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details! diff --git a/.changeset/brown-keys-raise.md b/.changeset/brown-keys-raise.md new file mode 100644 index 0000000000..1d288d9f57 --- /dev/null +++ b/.changeset/brown-keys-raise.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +The Kubernetes plugin now requests AKS access tokens from Azure when retrieving +objects from clusters configured with `authProvider: aks` and sets `auth.aks` in +its request bodies appropriately. diff --git a/.changeset/brown-meals-trade.md b/.changeset/brown-meals-trade.md new file mode 100644 index 0000000000..f6849fa19b --- /dev/null +++ b/.changeset/brown-meals-trade.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': minor +--- + +**BREAKING**: The OpenAPI commands are now found within the `schema openapi` sub-command. For example `yarn backstage-repo-tools schema:openapi:verify` is now `yarn backstage-repo-tools schema openapi verify`. diff --git a/.changeset/calm-buttons-type.md b/.changeset/calm-buttons-type.md new file mode 100644 index 0000000000..11b300a684 --- /dev/null +++ b/.changeset/calm-buttons-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Bump minimum required version of `vm2` to be 3.9.18 diff --git a/.changeset/calm-snails-trade.md b/.changeset/calm-snails-trade.md new file mode 100644 index 0000000000..0738b5aa37 --- /dev/null +++ b/.changeset/calm-snails-trade.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-bazaar': patch +--- + +Fixed `validateDOMNesting` warnings diff --git a/.changeset/chilled-goats-jump.md b/.changeset/chilled-goats-jump.md new file mode 100644 index 0000000000..3dd152712a --- /dev/null +++ b/.changeset/chilled-goats-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Correct command to create new plugins diff --git a/.changeset/chilled-pants-wonder.md b/.changeset/chilled-pants-wonder.md new file mode 100644 index 0000000000..501ced7cba --- /dev/null +++ b/.changeset/chilled-pants-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-openapi-utils': patch +--- + +Use permalinks for links including a line number reference diff --git a/.changeset/chilled-queens-wonder.md b/.changeset/chilled-queens-wonder.md new file mode 100644 index 0000000000..a10212792d --- /dev/null +++ b/.changeset/chilled-queens-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Ensure that entity cache state is only written to the database when actually changed diff --git a/.changeset/chilly-taxis-shop.md b/.changeset/chilly-taxis-shop.md new file mode 100644 index 0000000000..9e2e73a5e3 --- /dev/null +++ b/.changeset/chilly-taxis-shop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Remove object-hash dependency diff --git a/.changeset/clever-coins-deny.md b/.changeset/clever-coins-deny.md new file mode 100644 index 0000000000..0c1e2ade26 --- /dev/null +++ b/.changeset/clever-coins-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Use the semantic time tag for rendering world clocks on homepage headers. diff --git a/.changeset/cold-laws-turn.md b/.changeset/cold-laws-turn.md new file mode 100644 index 0000000000..6220f088db --- /dev/null +++ b/.changeset/cold-laws-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-events-backend-module-aws-sqs': minor +--- + +Allow endpoint configuration for sqs, enabling use of localstack for testing. diff --git a/.changeset/cool-dingos-repair.md b/.changeset/cool-dingos-repair.md new file mode 100644 index 0000000000..fe8377f5f1 --- /dev/null +++ b/.changeset/cool-dingos-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/theme': minor +--- + +Updates light theme's primary foreground and `running` status indicator colours to meet WCAG. Previously #2E77D0 changed to #1F5493. diff --git a/.changeset/curly-rats-fold.md b/.changeset/curly-rats-fold.md new file mode 100644 index 0000000000..c4cd8d514b --- /dev/null +++ b/.changeset/curly-rats-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Internal refactoring for performance in the service handlers diff --git a/.changeset/eleven-taxis-notice.md b/.changeset/eleven-taxis-notice.md new file mode 100644 index 0000000000..88a25991fa --- /dev/null +++ b/.changeset/eleven-taxis-notice.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +'@backstage/config-loader': patch +'@backstage/plugin-auth-backend': patch +--- + +Fixed the way that some request errors are thrown diff --git a/.changeset/extragavent-fast-fly.md b/.changeset/extragavent-fast-fly.md new file mode 100644 index 0000000000..697b5163ec --- /dev/null +++ b/.changeset/extragavent-fast-fly.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-badges': patch +'@backstage/plugin-badges-backend': minor +--- + +Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges. + +Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version. + +**BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options. diff --git a/.changeset/fair-suits-warn.md b/.changeset/fair-suits-warn.md new file mode 100644 index 0000000000..3edc2aa1ca --- /dev/null +++ b/.changeset/fair-suits-warn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Fix description links when clicking entry in radar. diff --git a/.changeset/fast-wasps-peel.md b/.changeset/fast-wasps-peel.md new file mode 100644 index 0000000000..075336a506 --- /dev/null +++ b/.changeset/fast-wasps-peel.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Enable strict config checking during `backstage-cli config:check` with the new `--strict` option which will surface schema errors. diff --git a/.changeset/fifty-grapes-explode.md b/.changeset/fifty-grapes-explode.md new file mode 100644 index 0000000000..999a105c4f --- /dev/null +++ b/.changeset/fifty-grapes-explode.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed a bug in the `queryEntities` endpoint that was causing filtered entities to be included in cursor requests. diff --git a/.changeset/five-lies-confess.md b/.changeset/five-lies-confess.md new file mode 100644 index 0000000000..eefd2bd6fa --- /dev/null +++ b/.changeset/five-lies-confess.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Improve the query for orphan pruning diff --git a/.changeset/fluffy-crabs-reply.md b/.changeset/fluffy-crabs-reply.md new file mode 100644 index 0000000000..0665dd864d --- /dev/null +++ b/.changeset/fluffy-crabs-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': minor +--- + +Upgrade to aws-sdk v3 and include OpenSearch Serverless support diff --git a/.changeset/fluffy-mirrors-happen.md b/.changeset/fluffy-mirrors-happen.md new file mode 100644 index 0000000000..6dce398217 --- /dev/null +++ b/.changeset/fluffy-mirrors-happen.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Kubernetes clusters now support `authProvider: aks`. When configured this way, +the `retrieveObjectsByServiceId` action will use the `auth.aks` value in the +request body as a bearer token to authenticate with Kubernetes. diff --git a/.changeset/friendly-frogs-drop.md b/.changeset/friendly-frogs-drop.md new file mode 100644 index 0000000000..682f1dbabb --- /dev/null +++ b/.changeset/friendly-frogs-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Change orphan cleanup task to only log a message if it deleted entities. diff --git a/.changeset/funny-cups-count.md b/.changeset/funny-cups-count.md new file mode 100644 index 0000000000..19dbc3b8bf --- /dev/null +++ b/.changeset/funny-cups-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-gcalendar': patch +--- + +Pass user info email scope on auth refresh to resolve invalid credentials error diff --git a/.changeset/giant-students-lie.md b/.changeset/giant-students-lie.md new file mode 100644 index 0000000000..fdcb406296 --- /dev/null +++ b/.changeset/giant-students-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Add ability to use `defaultNamespace` and `defaultKind` for scaffolder action `catalog:fetch` diff --git a/.changeset/gold-vans-sin.md b/.changeset/gold-vans-sin.md new file mode 100644 index 0000000000..d99cb9834d --- /dev/null +++ b/.changeset/gold-vans-sin.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Fix input schema validation issue for gitlab actions: + +- gitlab:group:ensureExists +- gitlab:projectAccessToken:create +- gitlab:projectDeployToken:create +- gitlab:projectVariable:create diff --git a/.changeset/heavy-penguins-burn.md b/.changeset/heavy-penguins-burn.md new file mode 100644 index 0000000000..b8c243a99b --- /dev/null +++ b/.changeset/heavy-penguins-burn.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-common': minor +'@backstage/plugin-scaffolder-react': minor +--- + +Add support for Markdown text blob outputs from templates diff --git a/.changeset/honest-comics-ring.md b/.changeset/honest-comics-ring.md new file mode 100644 index 0000000000..38a962c5d3 --- /dev/null +++ b/.changeset/honest-comics-ring.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-todo-backend': patch +--- + +Added OpenAPI schema diff --git a/.changeset/honest-turkeys-learn.md b/.changeset/honest-turkeys-learn.md new file mode 100644 index 0000000000..e4e6aae5fc --- /dev/null +++ b/.changeset/honest-turkeys-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Remove unnecessary join in the entity facets endpoint, exclude nulls diff --git a/.changeset/hungry-countries-enjoy.md b/.changeset/hungry-countries-enjoy.md new file mode 100644 index 0000000000..c14ec702b9 --- /dev/null +++ b/.changeset/hungry-countries-enjoy.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Fixed a bug in the Azure auth provider which prevented getting access tokens with multiple scopes for one resource diff --git a/.changeset/hungry-peas-sing.md b/.changeset/hungry-peas-sing.md new file mode 100644 index 0000000000..8c39d33ddd --- /dev/null +++ b/.changeset/hungry-peas-sing.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-openapi': patch +--- + +Fixed bug in `jsonSchemaRefPlaceholderResolver` where relative $ref files were resolved through file system instead of base URL of file diff --git a/.changeset/itchy-impalas-fold.md b/.changeset/itchy-impalas-fold.md new file mode 100644 index 0000000000..31840124ad --- /dev/null +++ b/.changeset/itchy-impalas-fold.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-events-backend-module-aws-sqs': patch +'@backstage/integration-aws-node': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/backend-common': patch +'@backstage/plugin-techdocs-node': patch +--- + +Standardize `@aws-sdk` v3 versions diff --git a/.changeset/khaki-brooms-kiss.md b/.changeset/khaki-brooms-kiss.md new file mode 100644 index 0000000000..76d4ff2cd2 --- /dev/null +++ b/.changeset/khaki-brooms-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-explore': patch +--- + +Display the title of the entity on the explore card if present, otherwise stick to the name diff --git a/.changeset/late-lizards-unite.md b/.changeset/late-lizards-unite.md new file mode 100644 index 0000000000..bf0a03de12 --- /dev/null +++ b/.changeset/late-lizards-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Added a new `noUndeclaredProperties` option to `SchemaLoader` to support enforcing that there are no extra keys when verifying config. diff --git a/.changeset/moody-shrimps-train.md b/.changeset/moody-shrimps-train.md new file mode 100644 index 0000000000..397500e2cb --- /dev/null +++ b/.changeset/moody-shrimps-train.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': minor +--- + +Add a new scaffolder action for gitlab to ensure a group exists diff --git a/.changeset/new-roses-fail.md b/.changeset/new-roses-fail.md new file mode 100644 index 0000000000..77495c5f54 --- /dev/null +++ b/.changeset/new-roses-fail.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-org': patch +--- + +Changed the MembersListCard component to allow displaying aggregated members when viewing a group. Now, a toggle switch can be displayed that lets you switch between showing direct members and aggregated members. + +To enable this new feature, set the showAggregateMembersToggle prop on EntityMembersListCard: + +```jsx +// In packages/app/src/components/catalog/EntityPage.tsx +const groupPage = ( + // ... + + // ... +); +``` diff --git a/.changeset/nice-ants-type.md b/.changeset/nice-ants-type.md new file mode 100644 index 0000000000..632d323213 --- /dev/null +++ b/.changeset/nice-ants-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': minor +--- + +**BREAKING**: AwsOrganizationCloudAccountProcessor.fromConfig now returns a promise instead of the instance directly. diff --git a/.changeset/nine-planes-carry.md b/.changeset/nine-planes-carry.md new file mode 100644 index 0000000000..414cc25680 --- /dev/null +++ b/.changeset/nine-planes-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-sites-backend': patch +--- + +Updated URL to `/health` and corrected typos in the `README.md` diff --git a/.changeset/ninety-eggs-lie.md b/.changeset/ninety-eggs-lie.md new file mode 100644 index 0000000000..83b04b82af --- /dev/null +++ b/.changeset/ninety-eggs-lie.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/theme': patch +--- + +Fix accessibility issue with Backstage Table's header style diff --git a/.changeset/ninety-mugs-sin.md b/.changeset/ninety-mugs-sin.md new file mode 100644 index 0000000000..6b71a00667 --- /dev/null +++ b/.changeset/ninety-mugs-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': minor +--- + + accepts InputProp property that can override keys from default diff --git a/.changeset/pre.json b/.changeset/pre.json index 759b1b40e2..bec2a1e2df 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.2.82", @@ -220,52 +220,89 @@ "@backstage/plugin-vault": "0.1.11", "@backstage/plugin-vault-backend": "0.3.0", "@backstage/plugin-xcmetrics": "0.2.37", - "@backstage/plugin-analytics-module-ga4": "0.0.0" + "@backstage/plugin-analytics-module-ga4": "0.0.0", + "@backstage/plugin-devtools": "0.0.0", + "@backstage/plugin-devtools-backend": "0.0.0", + "@backstage/plugin-devtools-common": "0.0.0" }, "changesets": [ "afraid-shirts-train", "backend-like-a-tattoo", "beige-tigers-matter", + "blue-eyes-run", "brown-eyes-yell", + "brown-keys-raise", + "calm-snails-trade", "calm-wolves-taste", + "chilled-goats-jump", + "chilled-pants-wonder", + "chilled-queens-wonder", "chilly-fireants-type", + "chilly-taxis-shop", "clean-lobsters-brake", "cold-drinks-exercise", + "cold-laws-turn", + "cool-dingos-repair", "create-app-1682437922", + "curly-rats-fold", "curvy-months-appear", "dry-boats-sniff", "dull-wombats-stare", "eighty-olives-live", + "extragavent-fast-fly", + "fair-suits-warn", + "fifty-grapes-explode", "fifty-laws-count", "five-tigers-whisper", + "fluffy-crabs-reply", + "fluffy-mirrors-happen", "fresh-moons-unite", "funny-trains-sniff", "good-lions-approve", "green-cheetahs-cover", "healthy-ladybugs-chew", + "heavy-penguins-burn", "honest-countries-deny", + "honest-turkeys-learn", "hungry-foxes-divide", + "hungry-peas-sing", + "khaki-brooms-kiss", "lazy-keys-work", "mighty-bears-glow", "mighty-cows-greet", "modern-pumpkins-join", "moody-lizards-beam", + "moody-shrimps-train", + "new-roses-fail", "nine-ties-smoke", + "ninety-eggs-lie", + "ninety-mugs-sin", "olive-turkeys-switch", "polite-dingos-train", "pretty-gifts-greet", "quick-flies-guess", + "quiet-bikes-smash", + "quiet-stingrays-fly", + "rare-elephants-arrive", "red-cheetahs-invite", "rotten-chefs-jog", "serious-hornets-remember", "six-mails-shout", "slimy-turkeys-return", "soft-readers-exist", + "stupid-seas-stare", "swift-fishes-run", "tame-games-rule", "thick-parents-pump", + "thick-pugs-rush", + "thin-ways-exist", + "thirty-jobs-sort", "tough-moles-whisper", + "twelve-zebras-repair", + "two-gorillas-drop", + "unlucky-deers-teach", "unlucky-plants-give", - "warm-lamps-tease" + "warm-lamps-tease", + "wet-dolphins-love" ] } diff --git a/.changeset/proud-chairs-sleep.md b/.changeset/proud-chairs-sleep.md new file mode 100644 index 0000000000..6df02d052d --- /dev/null +++ b/.changeset/proud-chairs-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': patch +--- + +`SearchPagination` now automatically resets the page cursor when the page limit is changed diff --git a/.changeset/quiet-bikes-smash.md b/.changeset/quiet-bikes-smash.md new file mode 100644 index 0000000000..ca751adaf7 --- /dev/null +++ b/.changeset/quiet-bikes-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-openapi-utils': patch +--- + +Corrected resolution of parameter nested schema to use central schemas. diff --git a/.changeset/quiet-stingrays-fly.md b/.changeset/quiet-stingrays-fly.md new file mode 100644 index 0000000000..536ee18a6f --- /dev/null +++ b/.changeset/quiet-stingrays-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-devops': minor +--- + +The getBuildRuns function now checks contains multiple comma-separated builds and splits them to send multiple requests for each and concatenates the results. diff --git a/.changeset/rare-elephants-arrive.md b/.changeset/rare-elephants-arrive.md new file mode 100644 index 0000000000..d27f763d1f --- /dev/null +++ b/.changeset/rare-elephants-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-puppetdb': patch +--- + +Fixes import paths and updates documentation diff --git a/.changeset/real-baboons-vanish.md b/.changeset/real-baboons-vanish.md new file mode 100644 index 0000000000..ca09cc9bf3 --- /dev/null +++ b/.changeset/real-baboons-vanish.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-jenkins-backend': minor +'@backstage/plugin-jenkins': minor +--- + +Updated rebuild to use Jenkins API replay build, which works for Jenkins jobs that have required parameters. Jenkins SDK could not be used for this request because it does not have support for replay. + +Added link to view build in Jenkins CI/CD table action column. diff --git a/.changeset/red-parrots-attack.md b/.changeset/red-parrots-attack.md new file mode 100644 index 0000000000..1530c17c8b --- /dev/null +++ b/.changeset/red-parrots-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +Fix a corner case where returned users are null for bots diff --git a/.changeset/serious-phones-flash.md b/.changeset/serious-phones-flash.md new file mode 100644 index 0000000000..4a4afe5d4e --- /dev/null +++ b/.changeset/serious-phones-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-module-addons-contrib': patch +--- + +Show cursor pointer when hovering on lightbox diff --git a/.changeset/shy-worms-enjoy.md b/.changeset/shy-worms-enjoy.md new file mode 100644 index 0000000000..cbfb2b5f7b --- /dev/null +++ b/.changeset/shy-worms-enjoy.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-user-settings': patch +'@backstage/plugin-auth-backend': patch +--- + +Fix config schema definition. diff --git a/.changeset/sixty-falcons-protect.md b/.changeset/sixty-falcons-protect.md new file mode 100644 index 0000000000..0bc77bfbd9 --- /dev/null +++ b/.changeset/sixty-falcons-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-badges-backend': patch +--- + +Removed some unused dependencies diff --git a/.changeset/spicy-spies-warn.md b/.changeset/spicy-spies-warn.md new file mode 100644 index 0000000000..3ed26a2cc3 --- /dev/null +++ b/.changeset/spicy-spies-warn.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-common': minor +--- + +Expose scaffolder permissions in new sub-aggregations. + +In addition to exporting a list of all scaffolder permissions in `scaffolderPermissions`, scaffolder-common now exports `scaffolderTemplatePermissions` and `scaffolderActionPermissions`, which contain subsets of the scaffolder permissions separated by resource type. diff --git a/.changeset/spotty-walls-serve.md b/.changeset/spotty-walls-serve.md new file mode 100644 index 0000000000..9b0da31a5f --- /dev/null +++ b/.changeset/spotty-walls-serve.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Fixed a bug in the Kubernetes proxy endpoint where requests to clusters configured with client-side auth providers would always fail with a 500 status. diff --git a/.changeset/stupid-seas-stare.md b/.changeset/stupid-seas-stare.md new file mode 100644 index 0000000000..f3d4f5e1f7 --- /dev/null +++ b/.changeset/stupid-seas-stare.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fix accessibility issue on controlled select input on tab navigation + keyboard enter/space action. diff --git a/.changeset/swift-students-type.md b/.changeset/swift-students-type.md new file mode 100644 index 0000000000..577fbeb8b6 --- /dev/null +++ b/.changeset/swift-students-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-openapi-utils': patch +--- + +Updated README to reflect changes in `@backstage/repo-tools`. diff --git a/.changeset/tame-games-rule.md b/.changeset/tame-games-rule.md deleted file mode 100644 index 408c8ca14b..0000000000 --- a/.changeset/tame-games-rule.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Pin some octokit dependencies to avoid failing typescript build under `tsc:full` diff --git a/.changeset/thick-pugs-rush.md b/.changeset/thick-pugs-rush.md new file mode 100644 index 0000000000..7f0171eb83 --- /dev/null +++ b/.changeset/thick-pugs-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added a persistent session store through the database diff --git a/.changeset/thin-ways-exist.md b/.changeset/thin-ways-exist.md new file mode 100644 index 0000000000..fd1ecb62a4 --- /dev/null +++ b/.changeset/thin-ways-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': patch +--- + +Fix text-overflow UI issue for Lifecycle spans in SearchFilter checkbox labels. diff --git a/.changeset/thirty-jobs-sort.md b/.changeset/thirty-jobs-sort.md new file mode 100644 index 0000000000..8a40302502 --- /dev/null +++ b/.changeset/thirty-jobs-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-common': patch +--- + +AKS access tokens can now be sent over the wire to the Kubernetes backend. diff --git a/.changeset/three-tools-pump.md b/.changeset/three-tools-pump.md new file mode 100644 index 0000000000..ac9d3c9909 --- /dev/null +++ b/.changeset/three-tools-pump.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fix accessibility issue with Edit Metadata Link on screen readers missing notice about opening in a new tab. diff --git a/.changeset/tiny-crews-pull.md b/.changeset/tiny-crews-pull.md new file mode 100644 index 0000000000..8d499ac5e6 --- /dev/null +++ b/.changeset/tiny-crews-pull.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Deprecated the use of React 16 diff --git a/.changeset/twelve-birds-boil.md b/.changeset/twelve-birds-boil.md new file mode 100644 index 0000000000..04317c26cd --- /dev/null +++ b/.changeset/twelve-birds-boil.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-adr': minor +--- + +render SupportButton only if config is set diff --git a/.changeset/twelve-zebras-repair.md b/.changeset/twelve-zebras-repair.md new file mode 100644 index 0000000000..8d74080c38 --- /dev/null +++ b/.changeset/twelve-zebras-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend': patch +--- + +Added an OpenAPI 3.0 spec and enforced schema-first model on the router. diff --git a/.changeset/two-gorillas-drop.md b/.changeset/two-gorillas-drop.md new file mode 100644 index 0000000000..72de66dbff --- /dev/null +++ b/.changeset/two-gorillas-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-explore': patch +--- + +Updated the example code in the "Customization" section of the README to make the imports match the components used. diff --git a/.changeset/unlucky-deers-teach.md b/.changeset/unlucky-deers-teach.md new file mode 100644 index 0000000000..b773e9090f --- /dev/null +++ b/.changeset/unlucky-deers-teach.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend-module-github': minor +--- + +Implement events support for `GithubMultiOrgEntityProvider` + +**BREAKING:** Passing in a custom `teamTransformer` will now correctly completely override the default transformer behavior diff --git a/.changeset/wet-dolphins-love.md b/.changeset/wet-dolphins-love.md new file mode 100644 index 0000000000..637d26ca70 --- /dev/null +++ b/.changeset/wet-dolphins-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Updated the `team.edited` event emitted from `GithubOrgEntityProvider` to also include teams description. diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index 83fba8aa56..83f05e390d 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -188,6 +188,7 @@ learnings Leasot lerna Lerna +lightbox limitranges LocalStack lockdown @@ -247,6 +248,7 @@ Olausson Oldsberg onboarding Onboarding +OpenSearch OpenShift orgs padding @@ -389,6 +391,7 @@ transpilers truthy TSDoc typeahead +Uffizzi ui unbreak Unconference diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86ac320ac7..ec0a791dcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: run: node scripts/verify-api-reference.js - name: verify openapi yaml file matches generated ts file - run: yarn backstage-repo-tools schema:openapi:verify + run: yarn backstage-repo-tools schema openapi verify - name: verify doc links run: node scripts/verify-links.js diff --git a/ADOPTERS.md b/ADOPTERS.md index b66fbea0f6..7db99119dc 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -132,7 +132,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Tink](https://tink.com/) | [Sebastian Olsson](https://github.com/Sebelino), [Błażej Szum](https://github.com/blazejszumtink), [Anders Eurenius Runvald](https://github.com/anders-er-at-tink) | Internal developer portal which provides templates for creating new Java or Go microservices seamlessly. Also includes a tech radar and a visualization of our CD pipeline. | | [Brandwatch](https://brandwatch.com) | [Stefan Buck](https://github.com/stefanbuck) | Our primary focus is on the service catalog. Backstage is replacing our homemade service catalog. The switch was quite simple due to the catalog processor API. | | [Laybuy](https://www.laybuy.com) | [Chris Simmons](https://github.com/contrarianchris) | Backstage is the heart of Laybuy’s new centralised Development Platform, bringing disparate development tools and experiences into a single easy-to-use portal. It simplifies software and API discovery, project scaffolding, and technical documentation, enabling us to embrace golden path development and automate software standards. | -| [Sendinblue](https://engineering.sendinblue.com/) | [Tanguy Antoine](mailto:antoine.tanguy@sendinblue.com) | Helps us drive the change at scale. Puts light on services, resources, and dependencies. One tool that rules them all through plugins we created for that purpose. We are aiming to put Backstage at the center of every developer's work (Actionable items, Debugging, Monitoring, Provisioning, etc...) to improve their happiness | +| [Brevo](https://engineering.brevo.com/) | [Tanguy Antoine](mailto:antoine.tanguy@brevo.com) | Helps us drive the change at scale. Puts light on services, resources, and dependencies. One tool that rules them all through plugins we created for that purpose. We are aiming to put Backstage at the center of every developer's work (Actionable items, Debugging, Monitoring, Provisioning, etc...) to improve their happiness | | [SafetyCulture](https://safetyculture.com/) | [@R-cen](https://github.com/R-cen), [@lachlancooper](https://github.com/lachlancooper), [@hkf57](https://github.com/hkf57) | Internal developer portal to provide a centralized place for engineers to see an overview of their team's services and information related to the service from other systems. Initially focused on the software catalog, techdocs and search. | | [Sana Life Science](https://sanalifescience.com) | [Joe Hillyard](mailto:joe@sanalifescience.com) | API Catalog, Tools Management & Control Hub | | [Ndustrial](https://ndustrial.io) | [Jonathan Skubic](mailto:jonathan@ndustrial.io) | Software Project Catalog | @@ -243,4 +243,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Chartboost](https://www.chartboost.com)| [@brucearctor](https://github.com/brucearctor), [@ArtemChekunov](https://github.com/ArtemChekunov) | We are building our internal develper portal on top of Backstage.| | [Quantum Metric](https://www.quantummetric.com/) | [Eric Irwin](https://www.linkedin.com/in/ericirwin1124/) | Backstage is used within our Developer Experience Platform (DXP) in order to increase self-service, standardization and discoverability across our Engineering teams. | | [VodafoneZiggo](https://www.vodafoneziggo.nl/) | [Peter Macdonald](https://github.com/Parsifal-M) | We use Backstage as our go-to platform for managing our internal tools and services. With Backstage, we can easily discover and access all the services we need to do our work, whether it's deploying code, managing infrastructure, or accessing documentation. We appreciate the standardized, consistent interface and the ability to easily create custom plugins to integrate with our existing workflows. Overall, Backstage is streamlining our internal operations and helps us work more efficiently as a team. | -| [Volvo Cars](https://www.volvocars.com) | [Martin Wänerskär](https://github.com/martin-wanerskar) | Internal developer portal with intent to unify infrastructure tooling, services, and developer documentation under a single, easy-to-use interface. | \ No newline at end of file +| [Volvo Cars](https://www.volvocars.com) | [Martin Wänerskär](https://github.com/martin-wanerskar) | Internal developer portal with intent to unify infrastructure tooling, services, and developer documentation under a single, easy-to-use interface. | +| [Mercantil Andina](https://www.mercantilandina.com.ar/) | [Sebastian Paredero](https://www.linkedin.com/in/sebastian-paredero/), [Matias Javier Gomez](https://www.linkedin.com/in/matias-javier-gomez-140540a3/), [Gustavo Wilgenhoff](https://github.com/wilgustavo) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | +| [Livelo](https://www.livelo.com.br) | [Felipe Nardon](https://www.linkedin.com/in/felipe-nardon-dos-santos/), [Yves Galvão](https://www.linkedin.com/in/yvesgalvao/), [Isaque Rocha](https://www.linkedin.com/in/isaque-rocha/) | Internal platform that aims to reduce the teams' cognitive load through self-service, abstracting processes that are now part of the teams' daily lives, enabling a centralized management of documentation, catalog, metrics and knowledge. | +| [Worten](https://www.worten.pt) | [@elisiariocouto](https://github.com/elisiariocouto) | We are proud to be an adopter of Backstage. Our goal is to make our Backstage installation the go-to location for every developer. To achieve this, we developed a custom plugin to populate Backstage's catalog with information from our internal deployment tool. As a result, we now have full visibility of all Worten systems, components, APIs, messaging topics, and documentation. We also migrated the internal deployment tool's Web UI to Backstage. Currently, we are working on Backstage's Software Templates feature. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7828791080..ed1baa133a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,10 +92,14 @@ For those contributors who have earned write access to the repository, when a pu ## Code of Conduct -This project adheres to the [Spotify FOSS Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code. +This project adheres to the [CNCF Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code. [code-of-conduct]: https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md ## Security Issues? See [SECURITY](SECURITY.md). + +## Accessibility + +We encourage you to catch any accessibility issues already in the development phase of new features to Backstage, see our [Accessibility documentation](./docs/accessibility/index.md) for more details. diff --git a/OWNERS.md b/OWNERS.md index 0a567ee269..25ada5853e 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -77,6 +77,20 @@ Scope: The TechDocs plugin and related tooling | Morgan Bentell | Spotify | Pulp Fiction | [agentbellnorm](http://github.com/agentbellnorm) | morganbentell#9030 | | Raghunandan Balachandran | Spotify | Pulp Fiction | [soapraj](http://github.com/soapraj) | raghunandanb#1114 | +## Incubating Project Areas + +These incubating project areas have shared ownership with @backstage/maintainers. + +### OpenAPI Tooling + +Team: @backstage/openapi-tooling-maintainers + +Scope: Tooling for frontend and backend schema-first OpenAPI development. + +| Name | Organization | GitHub | Discord | +| -------------- | ------------ | --------------------------------------- | ------------- | +| Aramis Sennyey | | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` | + ## Sponsors | Name | Organization | GitHub | Email | @@ -92,6 +106,7 @@ Scope: The TechDocs plugin and related tooling | Adam Harvey | Cisco | [adamdmharvey](https://github.com/adamdmharvey) | `adamharvey#3739` | | Andre Wanlin | Keyloop | [awanlin](https://github.com/awanlin) | `Ahhhndre#3095` | | Andrew Thauer | Wealthsimple | [andrewthauer](https://github.com/andrewthauer) | `andrewthauer#3060` | +| Aramis Sennyey | | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` | | Brian Fletcher | Roadie.io | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` | | Carlos Esteban Lopez Jaramillo | VMWare | [luchillo17](https://github.com/luchillo17) | `luchillo17#8777` | | David Tuite | Roadie.io | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` | diff --git a/README.md b/README.md index 4952eab10f..5676e262f8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) [![Codecov](https://img.shields.io/codecov/c/github/backstage/backstage)](https://codecov.io/gh/backstage/backstage) [![](https://img.shields.io/github/v/release/backstage/backstage)](https://github.com/backstage/backstage/releases) +[![Uffizzi](https://img.shields.io/endpoint?url=https%3A%2F%2Fapp.uffizzi.com%2Fapi%2Fv1%2Fpublic%2Fshields%2Fgithub.com%2Fbackstage%2Fbackstage)](https://app.uffizzi.com/ephemeral-environments/backstage/backstage) ## What is Backstage? diff --git a/contrib/catalog/ImmediateEntityProvider.ts b/contrib/catalog/ImmediateEntityProvider.ts index b66c62e330..33ae6697f6 100644 --- a/contrib/catalog/ImmediateEntityProvider.ts +++ b/contrib/catalog/ImmediateEntityProvider.ts @@ -9,8 +9,8 @@ import { DeferredEntity, EntityProvider, EntityProviderConnection, - parseEntityYaml, -} from '@backstage/plugin-catalog-backend'; +} from '@backstage/plugin-catalog-node'; +import { parseEntityYaml } from '@backstage/plugin-catalog-backend'; import bodyParser from 'body-parser'; import express from 'express'; import Router from 'express-promise-router'; diff --git a/docs/accessibility/index.md b/docs/accessibility/index.md new file mode 100644 index 0000000000..24685b95ce --- /dev/null +++ b/docs/accessibility/index.md @@ -0,0 +1,60 @@ +--- +id: index +title: Backstage Accessibility +description: Documentation on Backstage Accessibility +--- + +In an effort to bake accessibility practices further into our process of building features for Backstage, we support automated CI tests for some of our core features in the OSS project: Software Catalog, Software Templates, Search, and TechDocs. As these are just a few of the many plugins out there, we encourage you to consider the accessibility implications of your work on Backstage in order to build a great experience for everyone. + +## How to contribute + +There are multiple ways to contribute to making Backstage more accessible, you'll find below a list of examples to help you get started. + +### Run Lighthouse in CI on your plugin + +If your plugin lives in the [Backstage main repository](https://github.com/backstage/backstage/) you can modify the [urls in the Lighthouse config](https://github.com/backstage/backstage/blob/39ba2284d73885b7ca8290cb38e2b1e4d983c8d6/lighthouserc.js#L19-L34) to run the Lighthouse checks on urls where your plugin exists as well. E.g. + +```diff + ci: { + collect: { + url: [ ++ /** Your plugin paths */ ++ 'http://localhost:3000/your-plugin-path, + /** Software Catalog */ + 'http://localhost:3000/catalog', + 'http://localhost:3000/catalog-import', + 'http://localhost:3000/catalog/default/component/backstage', + ... + ], + settings: { + ... + }, + ... + }, + assert: { + ... + }, + }, +``` + +To make sure the [Accessibility Github workflow](https://github.com/backstage/backstage/blob/master/.github/workflows/verify_accessibility.yml) is running when changes are made to your plugin folders, modify the [list of paths](https://github.com/backstage/backstage/blob/10759b6ad2561bd86183ad940256f9a309c7a6b0/.github/workflows/verify_accessibility.yml#L7-L16). + +### Run the Lighthouse CLI locally when developing new features + +If you want to use the Lighthouse CLI and run the checks based on the config you can use the following command: + +``` +yarn dlx @lhci/cli@0.11.x autorun +``` + +> Note: running this command will use the [Lighthouse config](https://github.com/backstage/backstage/blob/39ba2284d73885b7ca8290cb38e2b1e4d983c8d6/lighthouserc.js#L19-L34) so make sure to adjust it to your needs if needed. + +### Use Lighthouse Github Action on your own repo + +If your Backstage plugin lives outside of the [Backstage main repository](https://github.com/backstage/backstage/), and you use GitHub Actions for continuous integration, we encourage you to add and modify the [Accessibility GitHub workflow](https://github.com/backstage/backstage/blob/master/.github/workflows/verify_accessibility.yml) to your needs. + +### Report identified issues + +It’s important to remember that automated checks can only catch a small number of accessibility issues, therefore we also encourage you to conduct manual testing of your plugins using Assistive technology (screen readers, alternative navigation, and screen magnifiers are a few examples). + +If you have identified accessibility issues and don’t have time to contribute a fix, please open an issue over at [Backstage Issues](https://github.com/backstage/backstage/issues) to let us know. diff --git a/docs/auth/gitlab/provider.md b/docs/auth/gitlab/provider.md index dc41617812..ed7fa6aec3 100644 --- a/docs/auth/gitlab/provider.md +++ b/docs/auth/gitlab/provider.md @@ -14,15 +14,17 @@ To support GitLab authentication, you must create an Application from the [GitLab settings](https://gitlab.com/-/profile/applications). The `Redirect URI` should point to your Backstage backend auth handler. -Settings for local development: - -- Name: Backstage (or your custom app name) -- Redirect URI: `http://localhost:7007/api/auth/gitlab/handler/frame` -- Scopes: `read_user` for sign-in. If you also need ID tokens (e.g. if you are - using the Kubernetes plugin and have clusters with `authProvider: oidc` and - [`oidcTokenProvider: -gitlab`](https://backstage.io/docs/features/kubernetes/configuration/#clustersoidctokenprovider-optional)), - add the `openid` scope. +1. Set Application Name to `backstage-dev` or something along those lines. +2. The Authorization Callback URL should match the redirect URI set in Backstage. + 1. Set this to `http://localhost:7007/api/auth/gitlab/handler/frame` for local development. + 2. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/api/auth/gitlab/handler/frame` for non-local deployments. + 3. Select the following scopes from the list: + - [x] `read_user` Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users. + - [x] `read_repository` Grants read-only access to repositories on private projects using Git-over-HTTP (not using the API). + - [x] `write_repository` Grants read-write access to repositories on private projects using Git-over-HTTP (not using the API). + - [x] `openid` Grants permission to authenticate with GitLab using OpenID Connect. Also gives read-only access to the user's profile and group memberships. + - [x] `profile` Grants read-only access to the user's profile data using OpenID Connect. + - [x] `email` Grants read-only access to the user's primary email address using OpenID Connect. ## Configuration diff --git a/docs/auth/index.md b/docs/auth/index.md index abe7fb8db6..b2aac47e2a 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -348,7 +348,28 @@ const gheAuthApiRef: ApiRef = }); ``` -The new ref is then used to add a new provider to the ApiFactory: +This new API ref will only work if you define an API factory for it. For example: + +```ts +createApiFactory({ + api: gheAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), +}); +``` + +The new API ref is then used to add a new provider to the ApiFactory: ```ts createApiFactory({ diff --git a/docs/features/kubernetes/authentication.md b/docs/features/kubernetes/authentication.md index d44d3a4a64..ddeabb5db0 100644 --- a/docs/features/kubernetes/authentication.md +++ b/docs/features/kubernetes/authentication.md @@ -66,6 +66,7 @@ as `401` or similar. The providers available as client side are: +- `aks` - `google` - `oidc` diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 458637f316..9280c4f020 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -104,12 +104,13 @@ cluster. Valid values are: | Value | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. | -| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. | +| `aks` | This will use a user's AKS access token from the [Microsoft auth provider](https://backstage.io/docs/auth/microsoft/provider) to access the Kubernetes API on AKS clusters. | | `aws` | This will use AWS credentials to access resources in EKS clusters | -| `googleServiceAccount` | This will use the Google Cloud service account credentials to access resources in clusters | | `azure` | This will use [Azure Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) to access resources in clusters | +| `google` | This will use a user's Google access token from the [Google auth provider](https://backstage.io/docs/auth/google/provider) to access the Kubernetes API on GKE clusters. | +| `googleServiceAccount` | This will use the Google Cloud service account credentials to access resources in clusters | | `oidc` | This will use [Oidc Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) to authenticate to the Kubernetes API. When this is used the `oidcTokenProvider` field should also be set. Please note the cluster must support OIDC, at the time of writing AKS clusters do not support OIDC. | +| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. | Check the [Kubernetes Authentication][4] section for additional explanation. diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 9fb57cb82f..1c9825d5ca 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -239,17 +239,18 @@ Example: `visits-tracking-service`, `CircleciBuildsDumpV2_avro_gcs` ### `namespace` [optional] -The ID of a namespace that the entity belongs to. This is a string that follows -the same format restrictions as `name` above. +The ID of a namespace that the entity belongs to. This field is optional, and +currently has no special semantics apart from bounding the name uniqueness +constraint if specified. It is reserved for future use and may get broader +semantic implication later. -This field is optional, and currently has no special semantics apart from -bounding the name uniqueness constraint if specified. It is reserved for future -use and may get broader semantic implication later. For now, it is recommended -to not specify a namespace unless you have specific need to do so. This means -the entity belongs to the `"default"` namespace. +For now, it is recommended to not specify a namespace unless you have specific +need to do so. This means the entity belongs to the `"default"` namespace. -Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities, -i.e. not Backstage specific but the same as in Kubernetes. +Namespaces must be sequences of `[a-zA-Z0-9]`, possibly separated by `-`, at +most 63 characters in total. Namespace names are case insensitive and will be rendered as lower case in most places. + +Example: `tracking-services`, `payment` ### `uid` [output] diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 15e6a833cc..67bcb6c179 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -510,10 +510,8 @@ take a look at, or you can Each individual step can output some variables that can be used in the scaffolder frontend for after the job is finished. This is useful for things -like linking to the entity that has been created with the backend, and also -linking to the created repository. - -The main two that are used are the following: +like linking to the entity that has been created with the backend, linking +to the created repository, or showing Markdown text blobs. ```yaml output: @@ -523,6 +521,10 @@ output: - title: Open in catalog icon: catalog entityRef: ${{ steps['register'].output.entityRef }} # link to the entity that has been ingested to the catalog + text: + - title: More information + content: | + **Entity URL:** `${{ steps['publish'].output.remoteUrl }}` ``` ## The templating syntax diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 7398e9a311..820f5ef394 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -21,6 +21,9 @@ you to use the [Contributors](contributors.md) guide to do a repository-based in - Access to a Unix-based operating system, such as Linux, MacOS or [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/) +- A GNU-like build environment available at the command line. + For example, on Debian/Ubuntu you will want to have the `make` and `build-essential` packages installed. + On MacOS, you will want to have run `xcode-select --install` to get the XCode command line build tooling in place. - An account with elevated rights to install the dependencies - `curl` or `wget` installed - Node.js [Active LTS Release](https://nodejs.org/en/blog/release/) installed using one of these diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index 821d35db15..32eb2b3de8 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -24,7 +24,7 @@ app: # env: 'staging' ``` -If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/master/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/master/packages/app/public/index.html#L64) to include the Datadog RUM `init()` section manually. +If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/packages/app/public/index.html#L69) to include the Datadog RUM `init()` section manually. The `clientToken` and `applicationId` are generated from the Datadog RUM page following diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index ccffe60bd1..39532e806f 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -65,6 +65,42 @@ export default async function createPlugin( } ``` +Alternatively, if you wish to ingest data from multiple GitHub organizations you can use +the `GithubMultiOrgEntityProvider` instead. Note that by default, this provider will namespace +groups according to the org they originate from to avoid potential name duplicates: + +```ts title="packages/backend/src/plugins/catalog.ts" +/* highlight-add-next-line */ +import { GithubMultiOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const builder = await CatalogBuilder.create(env); + + /* highlight-add-start */ + // The GitHub URL below needs to match a configured integrations.github entry + // specified in your app-config. + builder.addEntityProvider( + GithubMultiOrgEntityProvider.fromConfig(env.config, { + id: 'production', + githubUrl: 'https://github.com', + // Set the following to list the GitHub orgs you wish to ingest from. You can + // also omit this option to ingest all orgs accessible by your GitHub integration + orgs: ['org-a', 'org-b'], + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: { minutes: 60 }, + timeout: { minutes: 15 }, + }), + }), + ); + /* highlight-add-end */ + + // .. +} +``` + ## Installation with Events Support Please follow the installation instructions at @@ -110,6 +146,42 @@ export default async function createPlugin( } ``` +Or, alternatively, if using the `GithubMultiOrgEntityProvider`: + +```ts title="packages/backend/src/plugins/catalog.ts" +/* highlight-add-next-line */ +import { GithubMultiOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const builder = await CatalogBuilder.create(env); + + /* highlight-add-start */ + // The GitHub URL below needs to match a configured integrations.github entry + // specified in your app-config. + builder.addEntityProvider( + GithubMultiOrgEntityProvider.fromConfig(env.config, { + id: 'production', + githubUrl: 'https://github.com', + // Set the following to list the GitHub orgs you wish to ingest from. You can + // also omit this option to ingest all orgs accessible by your GitHub integration + orgs: ['org-a', 'org-b'], + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: { minutes: 60 }, + timeout: { minutes: 15 }, + }), + // Pass in the eventBroker to allow this provider to subscribe to GitHub events + eventBroker: env.eventBroker, + }), + ); + /* highlight-add-end */ + + // .. +} +``` + You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks). The webhook will need to be configured to forward `organization`,`team` and `membership` events. diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index c4c057ffe6..66a1855fca 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -290,7 +290,8 @@ Options: --package Only load config schema that applies to the given package --lax Do not require environment variables to be set --frontend Only validate the frontend configuration - --deprecated List all deprecated configuration settings + --deprecated Output deprecated configuration settings + --strict Ensure that the provided config(s) has no errors and does not contain keys not in the schema. --config Config files to load instead of app-config.yaml (default: []) -h, --help display help for command ``` diff --git a/docs/overview/support.md b/docs/overview/support.md index a0444c4d34..c968405a99 100644 --- a/docs/overview/support.md +++ b/docs/overview/support.md @@ -15,7 +15,7 @@ description: Support and Community Details and Links - [Code of Conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md) - This is how we roll. - [Blog](https://backstage.io/blog/) - Announcements and updates. -- [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to +- [Newsletter](https://info.backstage.spotify.com/newsletter_subscribe) - Subscribe to our email newsletter. - Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star! ❤️ diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md index 6b4ac98646..a4d51b64d0 100644 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md @@ -31,11 +31,12 @@ export const todoListUpdatePermission = createPermission({ /* highlight-remove-next-line */ export const todoListPermissions = [todoListCreatePermission]; -/* highlight-add-next-line */ +/* highlight-add-start */ export const todoListPermissions = [ todoListCreatePermission, todoListUpdatePermission, ]; +/* highlight-add-end */ ``` Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource. diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md index 0cae08a668..a9e62040c5 100644 --- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md +++ b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md @@ -66,16 +66,12 @@ export const todoListReadPermission = createPermission({ }); /* highlight-add-end */ -/* highlight-add-next-line */ -export const todoListPermissions = [ - todoListCreatePermission, - todoListUpdatePermission, -]; -/* highlight-add-next-line */ export const todoListPermissions = [ todoListCreatePermission, todoListUpdatePermission, + /* highlight-add-start */ todoListReadPermission, + /* highlight-add-end */ ]; ``` diff --git a/docs/publishing.md b/docs/publishing.md index 142e019fb2..12e4878d5a 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -11,7 +11,7 @@ npm. ### Creating a new release Releases are handled by changesets and trigger whenever the "Version Packages" -PR is merged. This is typically done every Thursday around noon CET. +PR is merged. This is typically done every Tuesday around noon CET. ## Emergency Release Process diff --git a/docs/releases/v1.14.0-next.2-changelog.md b/docs/releases/v1.14.0-next.2-changelog.md new file mode 100644 index 0000000000..95bbc63953 --- /dev/null +++ b/docs/releases/v1.14.0-next.2-changelog.md @@ -0,0 +1,1334 @@ +# Release v1.14.0-next.2 + +## @backstage/theme@0.3.0-next.0 + +### Minor Changes + +- 98c0c199b15: Updates light theme's primary foreground and `running` status indicator colours to meet WCAG. Previously #2E77D0 changed to #1F5493. + +### Patch Changes + +- 83b45f9df50: Fix accessibility issue with Backstage Table's header style + +## @backstage/plugin-azure-devops@0.3.0-next.2 + +### Minor Changes + +- 877df261085: The getBuildRuns function now checks contains multiple comma-separated builds and splits them to send multiple requests for each and concatenates the results. + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-badges-backend@0.2.0-next.2 + +### Minor Changes + +- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges. + + Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version. + + **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.7 + +## @backstage/plugin-catalog-backend-module-github@0.3.0-next.2 + +### Minor Changes + +- 970678adbe2: Implement events support for `GithubMultiOrgEntityProvider` + + **BREAKING:** Passing in a custom `teamTransformer` will now correctly completely override the default transformer behavior + +### Patch Changes + +- bd101cefd37: Updated the `team.edited` event emitted from `GithubOrgEntityProvider` to also include teams description. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/config@1.0.7 + +## @backstage/plugin-devtools@0.1.0-next.0 + +### Minor Changes + +- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-devtools-common@0.1.0-next.0 + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + +## @backstage/plugin-devtools-backend@0.1.0-next.0 + +### Minor Changes + +- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-devtools-common@0.1.0-next.0 + - @backstage/config@1.0.7 + +## @backstage/plugin-devtools-common@0.1.0-next.0 + +### Minor Changes + +- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details! + +## @backstage/plugin-events-backend-module-aws-sqs@0.2.0-next.2 + +### Minor Changes + +- 2c5661f3899: Allow endpoint configuration for sqs, enabling use of localstack for testing. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.7 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.2.0-next.2 + +### Minor Changes + +- 439e2986be1: Add a new scaffolder action for gitlab to ensure a group exists + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + +## @backstage/plugin-scaffolder-common@1.3.0-next.0 + +### Minor Changes + +- 82e10a6939c: Add support for Markdown text blob outputs from templates + +## @backstage/plugin-scaffolder-react@1.4.0-next.2 + +### Minor Changes + +- 82e10a6939c: Add support for Markdown text blob outputs from templates + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/plugin-scaffolder-common@1.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.3.0-next.2 + +### Minor Changes + +- 3d72bdb41c7: Upgrade to aws-sdk v3 and include OpenSearch Serverless support + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.7 + - @backstage/integration-aws-node@0.1.2 + +## @backstage/plugin-search-react@1.6.0-next.2 + +### Minor Changes + +- 1ce7f84b2e8: accepts InputProp property that can override keys from default + +### Patch Changes + +- adb31096bc2: Fix text-overflow UI issue for Lifecycle spans in SearchFilter checkbox labels. +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/app-defaults@1.3.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + +## @backstage/backend-openapi-utils@0.0.2-next.1 + +### Patch Changes + +- fe16bd39e83: Use permalinks for links including a line number reference +- 021cfbb5152: Corrected resolution of parameter nested schema to use central schemas. + +## @backstage/core-components@0.13.1-next.1 + +### Patch Changes + +- 83b45f9df50: Fix accessibility issue with Backstage Table's header style +- e97769f7c0b: Fix accessibility issue on controlled select input on tab navigation + keyboard enter/space action. +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/create-app@0.5.1-next.2 + +### Patch Changes + +- 1d5e42655cd: Correct command to create new plugins + +## @backstage/dev-utils@1.0.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/app-defaults@1.3.1-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/test-utils@1.3.1-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/integration-react@1.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/test-utils@1.3.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/config@1.0.7 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + +## @backstage/plugin-adr@0.5.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-airbrake@0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/dev-utils@1.0.15-next.2 + - @backstage/test-utils@1.3.1-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-allure@0.1.34-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-analytics-module-ga@0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-analytics-module-ga4@0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-apache-airflow@0.2.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-api-docs@0.9.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog@1.11.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-apollo-explorer@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-auth-backend@0.18.3-next.2 + +### Patch Changes + +- 3ffcdac7d07: Added a persistent session store through the database +- Updated dependencies + - @backstage/config@1.0.7 + +## @backstage/plugin-azure-sites@0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-badges@0.2.42-next.2 + +### Patch Changes + +- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges. + + Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version. + + **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options. + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-bazaar@0.2.8-next.2 + +### Patch Changes + +- 900880ab7c3: Fixed `validateDOMNesting` warnings +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/cli@0.22.7-next.0 + - @backstage/plugin-catalog@1.11.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-bitrise@0.1.45-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-catalog@1.11.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-catalog-backend@1.9.1-next.2 + +### Patch Changes + +- ce8d203235b: Ensure that entity cache state is only written to the database when actually changed +- 485a6c5f7b5: Internal refactoring for performance in the service handlers +- 3587a968dcd: Fixed a bug in the `queryEntities` endpoint that was causing filtered entities to be included in cursor requests. +- 12a345317ab: Remove unnecessary join in the entity facets endpoint, exclude nulls +- Updated dependencies + - @backstage/plugin-scaffolder-common@1.3.0-next.0 + - @backstage/config@1.0.7 + +## @backstage/plugin-catalog-backend-module-aws@0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.3-next.0 + - @backstage/config@1.0.7 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.3.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/config@1.0.7 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.11-next.2 + +### Patch Changes + +- accaceadffa: Fixed bug in `jsonSchemaRefPlaceholderResolver` where relative $ref files were resolved through file system instead of base URL of file +- Updated dependencies + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/config@1.0.7 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.2-next.2 + +### Patch Changes + +- 95b2168d71b: Fixes import paths and updates documentation +- Updated dependencies + - @backstage/config@1.0.7 + +## @backstage/plugin-catalog-graph@0.2.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-catalog-import@0.9.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.1-next.1 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-catalog-react@1.6.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + +## @backstage/plugin-cicd-statistics@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-cicd-statistics@0.1.20-next.2 + +## @backstage/plugin-circleci@0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-cloudbuild@0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-code-climate@0.1.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-code-coverage@0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-codescene@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-config-schema@0.1.41-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-cost-insights@0.12.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-dynatrace@5.0.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-entity-feedback@0.2.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-entity-validation@0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-explore@0.4.3-next.2 + +### Patch Changes + +- 4851581deb6: Display the title of the entity on the explore card if present, otherwise stick to the name +- a6025e25d99: Updated the example code in the "Customization" section of the README to make the imports match the components used. +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/plugin-explore-react@0.0.28 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-firehydrant@0.2.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-fossa@0.2.50-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-gcalendar@0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-gcp-projects@0.3.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-git-release-manager@0.3.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-github-actions@0.5.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-github-deployments@0.1.49-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-github-issues@0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-github-pull-requests-board@0.1.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-gitops-profiles@0.3.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-gocd@0.1.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-graphiql@0.2.50-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-graphql-voyager@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-home@0.5.2-next.2 + +### Patch Changes + +- acca8966465: Remove object-hash dependency +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-ilert@0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-jenkins@0.7.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-kafka@0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-kubernetes@0.9.0-next.2 + +### Patch Changes + +- c7bad1005ba: The Kubernetes plugin now requests AKS access tokens from Azure when retrieving + objects from clusters configured with `authProvider: aks` and sets `auth.aks` in + its request bodies appropriately. +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-kubernetes-common@0.6.3-next.0 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-kubernetes-backend@0.11.0-next.2 + +### Patch Changes + +- 05f1d74539d: Kubernetes clusters now support `authProvider: aks`. When configured this way, + the `retrieveObjectsByServiceId` action will use the `auth.aks` value in the + request body as a bearer token to authenticate with Kubernetes. +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.3-next.0 + - @backstage/config@1.0.7 + - @backstage/integration-aws-node@0.1.2 + +## @backstage/plugin-kubernetes-common@0.6.3-next.0 + +### Patch Changes + +- 05f1d74539d: AKS access tokens can now be sent over the wire to the Kubernetes backend. + +## @backstage/plugin-lighthouse@0.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-linguist@0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-microsoft-calendar@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-newrelic@0.3.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-newrelic-dashboard@0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-octopus-deploy@0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-org@0.6.8-next.2 + +### Patch Changes + +- 6e387c077a4: Changed the MembersListCard component to allow displaying aggregated members when viewing a group. Now, a toggle switch can be displayed that lets you switch between showing direct members and aggregated members. + + To enable this new feature, set the showAggregateMembersToggle prop on EntityMembersListCard: + + ```jsx + // In packages/app/src/components/catalog/EntityPage.tsx + const groupPage = ( + // ... + + // ... + ); + ``` + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-org-react@0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-pagerduty@0.5.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-periskop@0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-playlist@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + +## @backstage/plugin-puppetdb@0.1.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-rollbar@0.4.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-scaffolder@1.13.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/plugin-scaffolder-common@1.3.0-next.0 + - @backstage/plugin-scaffolder-react@1.4.0-next.2 + - @backstage/core-components@0.13.1-next.1 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + +## @backstage/plugin-scaffolder-backend@1.13.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/plugin-scaffolder-common@1.3.0-next.0 + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.13.2-next.2 + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.13.2-next.2 + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.13.2-next.2 + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + +## @backstage/plugin-scaffolder-node@0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-common@1.3.0-next.0 + +## @backstage/plugin-search@1.3.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-search-backend@1.3.1-next.2 + +### Patch Changes + +- 021cfbb5152: Added an OpenAPI 3.0 spec and enforced schema-first model on the router. +- Updated dependencies + - @backstage/config@1.0.7 + +## @backstage/plugin-sentry@0.5.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-shortcuts@0.3.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-sonarqube@0.6.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-splunk-on-call@0.4.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-stack-overflow@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home@0.5.2-next.2 + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-stackstorm@0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-tech-insights@0.3.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-tech-radar@0.6.4-next.2 + +### Patch Changes + +- be4fa53fab8: Fix description links when clicking entry in radar. +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-techdocs@1.6.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/test-utils@1.3.1-next.2 + - @backstage/plugin-catalog@1.11.0-next.2 + - @backstage/plugin-techdocs@1.6.2-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-techdocs-react@1.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-todo@0.2.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-user-settings@0.7.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-vault@0.1.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + +## @backstage/plugin-xcmetrics@0.2.38-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + +## example-app@0.2.83-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-devtools@0.1.0-next.0 + - @backstage/plugin-kubernetes@0.9.0-next.2 + - @backstage/plugin-home@0.5.2-next.2 + - @backstage/theme@0.3.0-next.0 + - @backstage/plugin-badges@0.2.42-next.2 + - @backstage/plugin-tech-radar@0.6.4-next.2 + - @backstage/plugin-scaffolder-react@1.4.0-next.2 + - @backstage/plugin-explore@0.4.3-next.2 + - @backstage/plugin-org@0.6.8-next.2 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/plugin-azure-devops@0.3.0-next.2 + - @backstage/plugin-stack-overflow@0.1.15-next.2 + - @backstage/app-defaults@1.3.1-next.2 + - @backstage/cli@0.22.7-next.0 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-airbrake@0.3.18-next.2 + - @backstage/plugin-api-docs@0.9.3-next.2 + - @backstage/plugin-azure-sites@0.1.7-next.2 + - @backstage/plugin-catalog-graph@0.2.30-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/plugin-circleci@0.3.18-next.2 + - @backstage/plugin-cloudbuild@0.3.18-next.2 + - @backstage/plugin-code-coverage@0.2.11-next.2 + - @backstage/plugin-cost-insights@0.12.7-next.2 + - @backstage/plugin-dynatrace@5.0.0-next.2 + - @backstage/plugin-entity-feedback@0.2.1-next.2 + - @backstage/plugin-gcalendar@0.3.14-next.1 + - @backstage/plugin-gcp-projects@0.3.37-next.1 + - @backstage/plugin-github-actions@0.5.18-next.2 + - @backstage/plugin-gocd@0.1.24-next.2 + - @backstage/plugin-graphiql@0.2.50-next.1 + - @backstage/plugin-jenkins@0.7.17-next.2 + - @backstage/plugin-kafka@0.3.18-next.2 + - @backstage/plugin-lighthouse@0.4.3-next.2 + - @backstage/plugin-linguist@0.1.3-next.2 + - @backstage/plugin-microsoft-calendar@0.1.3-next.1 + - @backstage/plugin-newrelic@0.3.36-next.1 + - @backstage/plugin-octopus-deploy@0.2.0-next.2 + - @backstage/plugin-pagerduty@0.5.11-next.2 + - @backstage/plugin-playlist@0.1.9-next.2 + - @backstage/plugin-puppetdb@0.1.1-next.2 + - @backstage/plugin-rollbar@0.4.18-next.2 + - @backstage/plugin-scaffolder@1.13.1-next.2 + - @backstage/plugin-search@1.3.0-next.2 + - @backstage/plugin-sentry@0.5.3-next.2 + - @backstage/plugin-shortcuts@0.3.10-next.2 + - @backstage/plugin-stackstorm@0.1.2-next.1 + - @backstage/plugin-tech-insights@0.3.10-next.2 + - @backstage/plugin-techdocs@1.6.2-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.13-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/plugin-todo@0.2.20-next.2 + - @backstage/plugin-user-settings@0.7.3-next.2 + - @backstage/plugin-apache-airflow@0.2.11-next.1 + - @backstage/plugin-catalog-import@0.9.8-next.2 + - @backstage/plugin-newrelic-dashboard@0.2.11-next.2 + - @backstage/config@1.0.7 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + - @internal/plugin-catalog-customized@0.0.10-next.2 + +## example-backend@0.2.83-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-devtools-backend@0.1.0-next.0 + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/plugin-badges-backend@0.2.0-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.0-next.2 + - @backstage/plugin-kubernetes-backend@0.11.0-next.2 + - @backstage/plugin-auth-backend@0.18.3-next.2 + - @backstage/plugin-search-backend@1.3.1-next.2 + - example-app@0.2.83-next.2 + - @backstage/plugin-scaffolder-backend@1.13.2-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.14-next.2 + - @backstage/config@1.0.7 + +## example-backend-next@0.0.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/plugin-kubernetes-backend@0.11.0-next.2 + - @backstage/plugin-search-backend@1.3.1-next.2 + - @backstage/plugin-scaffolder-backend@1.13.2-next.2 + +## e2e-test@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.1-next.2 + +## techdocs-cli-embedded-app@0.2.82-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/app-defaults@1.3.1-next.2 + - @backstage/cli@0.22.7-next.0 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/test-utils@1.3.1-next.2 + - @backstage/plugin-catalog@1.11.0-next.2 + - @backstage/plugin-techdocs@1.6.2-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/config@1.0.7 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + +## @internal/plugin-catalog-customized@0.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.11.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + +## @internal/plugin-todo-list@1.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 diff --git a/microsite/data/plugins/aws-app-development.yaml b/microsite/data/plugins/aws-app-development.yaml new file mode 100644 index 0000000000..3944bbb4e0 --- /dev/null +++ b/microsite/data/plugins/aws-app-development.yaml @@ -0,0 +1,10 @@ +--- +title: AWS App Developer Tools +author: Amazon Web Services +authorUrl: https://aws.amazon.com/ +category: Infrastructure +description: Create and manage AWS Apps within Backstage +documentation: https://github.com/awslabs/app-development-for-backstage-io-on-aws#readme +iconUrl: https://github.com/awslabs/app-development-for-backstage-io-on-aws/blob/main/docs/images/AWS_logo.png?raw=true +npmPackageName: '@aws/plugin-aws-apps-for-backstage' +addedDate: '2023-05-10' diff --git a/microsite/data/plugins/catalog-backend-module-puppetdb.yaml b/microsite/data/plugins/catalog-backend-module-puppetdb.yaml index 3eac117e19..e7a15ecd4a 100644 --- a/microsite/data/plugins/catalog-backend-module-puppetdb.yaml +++ b/microsite/data/plugins/catalog-backend-module-puppetdb.yaml @@ -5,7 +5,7 @@ authorUrl: https://github.com/tdabasinskas category: Configuration Management description: Import nodes from PuppetDB into Backstage as Resource Entities documentation: https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-puppetdb/README.md -iconUrl: https://avatars.githubusercontent.com/u/234268?s=200&v=4 +iconUrl: /img/puppet.png npmPackageName: '@backstage/plugin-catalog-backend-module-puppetdb' tags: - puppet diff --git a/microsite/data/plugins/dependencytrack b/microsite/data/plugins/dependencytrack.yaml similarity index 100% rename from microsite/data/plugins/dependencytrack rename to microsite/data/plugins/dependencytrack.yaml diff --git a/microsite/data/plugins/kubernetes-provider.yaml b/microsite/data/plugins/kubernetes-provider.yaml new file mode 100644 index 0000000000..e0971d55be --- /dev/null +++ b/microsite/data/plugins/kubernetes-provider.yaml @@ -0,0 +1,10 @@ +--- +title: Kubernetes Entity Provider +author: Antoine Dao +authorUrl: https://github.com/antoinedao +category: Kubernetes +description: Import Kubernetes resources into Backstage Components +documentation: https://github.com/AntoineDao/backstage-provider-kubernetes#readme +iconUrl: https://avatars.githubusercontent.com/u/13629408 +npmPackageName: https://www.npmjs.com/package/@antoinedao/backstage-provider-kubernetes +addedDate: '2023-04-10' diff --git a/microsite/data/plugins/xkcd.yaml b/microsite/data/plugins/xkcd.yaml new file mode 100644 index 0000000000..370718a08f --- /dev/null +++ b/microsite/data/plugins/xkcd.yaml @@ -0,0 +1,10 @@ +--- +title: xkcd comic +author: Vity +authorUrl: https://github.com/Vity01 +category: Humor +description: xkcd comic within Backstage +documentation: https://github.com/Vity01/backstage-xkcd/ +iconUrl: https://raw.githubusercontent.com/Vity01/backstage-xkcd/main/docs/xkcd.svg +npmPackageName: 'backstage-plugin-xkcd' +addedDate: '2023-05-05' diff --git a/microsite/docusaurus.config.js b/microsite/docusaurus.config.js index e6137b572b..d0f6bb277f 100644 --- a/microsite/docusaurus.config.js +++ b/microsite/docusaurus.config.js @@ -241,7 +241,7 @@ module.exports = { }, { label: 'Subscribe to our newsletter', - to: 'https://mailchi.mp/spotify/backstage-community', + to: 'https://info.backstage.spotify.com/newsletter_subscribe', }, { label: 'CNCF Incubation', diff --git a/microsite/package.json b/microsite/package.json index 6e20cad4fa..601a234688 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -17,7 +17,7 @@ "docusaurus": "docusaurus" }, "devDependencies": { - "@docusaurus/module-type-aliases": "0.0.0-5566", + "@docusaurus/module-type-aliases": "0.0.0-5569", "@spotify/prettier-config": "^14.0.0", "@tsconfig/docusaurus": "^1.0.6", "@types/webpack-env": "^1.18.0", @@ -28,9 +28,9 @@ }, "prettier": "@spotify/prettier-config", "dependencies": { - "@docusaurus/core": "0.0.0-5566", - "@docusaurus/plugin-client-redirects": "0.0.0-5566", - "@docusaurus/preset-classic": "0.0.0-5566", + "@docusaurus/core": "0.0.0-5569", + "@docusaurus/plugin-client-redirects": "0.0.0-5569", + "@docusaurus/preset-classic": "0.0.0-5569", "@swc/core": "^1.3.46", "clsx": "^1.1.1", "docusaurus-plugin-sass": "^0.2.3", diff --git a/microsite/sidebars.json b/microsite/sidebars.json index cf2f9b7b56..1515c483b9 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -408,6 +408,7 @@ "label": "Core Services", "items": ["backend-system/core-services/index"] } - ] + ], + "Accessibility": ["accessibility/index"] } } diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 10028fc4b4..c16f65240e 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -1620,9 +1620,9 @@ __metadata: languageName: node linkType: hard -"@docusaurus/core@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/core@npm:0.0.0-5566" +"@docusaurus/core@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/core@npm:0.0.0-5569" dependencies: "@babel/core": ^7.20.12 "@babel/generator": ^7.21.1 @@ -1634,13 +1634,13 @@ __metadata: "@babel/runtime": ^7.21.0 "@babel/runtime-corejs3": ^7.21.0 "@babel/traverse": ^7.21.2 - "@docusaurus/cssnano-preset": 0.0.0-5566 - "@docusaurus/logger": 0.0.0-5566 - "@docusaurus/mdx-loader": 0.0.0-5566 + "@docusaurus/cssnano-preset": 0.0.0-5569 + "@docusaurus/logger": 0.0.0-5569 + "@docusaurus/mdx-loader": 0.0.0-5569 "@docusaurus/react-loadable": 5.5.2 - "@docusaurus/utils": 0.0.0-5566 - "@docusaurus/utils-common": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/utils": 0.0.0-5569 + "@docusaurus/utils-common": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 "@slorber/static-site-generator-webpack-plugin": ^4.0.7 "@svgr/webpack": ^6.5.1 autoprefixer: ^10.4.13 @@ -1700,41 +1700,41 @@ __metadata: react-dom: ^16.8.4 || ^17.0.0 bin: docusaurus: bin/docusaurus.mjs - checksum: c85ea103cd3a14dc340533d1cad0627736d87e4336b5919adabd23627958eed108fddb2dd45066944185ebe52a8648a4463a9587804f3a537f0a8deaea3e835e + checksum: 573746ba85750381b110a98cfb529d8c961523b735c1945016bf8f79ba2cde38d08ecda88dc1be4852d8c9a49121e23ec964b05354fd6f6d26e6f31e71f175c1 languageName: node linkType: hard -"@docusaurus/cssnano-preset@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/cssnano-preset@npm:0.0.0-5566" +"@docusaurus/cssnano-preset@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/cssnano-preset@npm:0.0.0-5569" dependencies: cssnano-preset-advanced: ^5.3.10 postcss: ^8.4.21 postcss-sort-media-queries: ^4.3.0 tslib: ^2.5.0 - checksum: 9aece7f723e7bdeecaa0cc9ce956ef590dfd1c11200f9799236ce80ad94f4653c717a0f16d8694a9f635fb21e0e5bbf3093ba59f8996c012b71062560d9e0fd3 + checksum: 11408a0f9eb1a4a83b2045a0172ef1fbf0523a99fa9d81a8efe9a3a968b449887616ad8d95b0abb55b2bf82a178193c20c0304942c4f2ba51b693b9209031204 languageName: node linkType: hard -"@docusaurus/logger@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/logger@npm:0.0.0-5566" +"@docusaurus/logger@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/logger@npm:0.0.0-5569" dependencies: chalk: ^4.1.2 tslib: ^2.5.0 - checksum: aab288cf6f7c062bb54de296723bbd8deea3029267c49e3f2fb6ab86b462256c4cff805debfd93bd3b35daaefdb9f5bb1a07876434e0b4887e0ec6cf159594d0 + checksum: 26be75d7180d9b9d525ce5014e1e24c69fd54d1707cabc5acc76b47e3282cfb185613fb4d27f32b5cced92fe80384c68e2d991cf4cabbecca331f570c3e10d9b languageName: node linkType: hard -"@docusaurus/mdx-loader@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/mdx-loader@npm:0.0.0-5566" +"@docusaurus/mdx-loader@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/mdx-loader@npm:0.0.0-5569" dependencies: "@babel/parser": ^7.21.2 "@babel/traverse": ^7.21.2 - "@docusaurus/logger": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/logger": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 "@mdx-js/mdx": ^2.1.5 escape-html: ^1.0.3 estree-util-value-to-estree: ^2.1.0 @@ -1757,16 +1757,16 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: d4e82a3bdf8eefe7c3996c9ac3860a60f6bb028ab60dce31cef199d22da606631e7eacfeacd4e51c0181d3649026e081c353dea5f01317ec05b48c20b4e8cc39 + checksum: bcf3b58ea3840d648ddc81c090c9905d559f642d8dc495f3fa86b377ae0bdabb6e1486ae2b258efa459a8bcf1da729a4a3a0c144f0de5dc083155b6d5c83099c languageName: node linkType: hard -"@docusaurus/module-type-aliases@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/module-type-aliases@npm:0.0.0-5566" +"@docusaurus/module-type-aliases@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/module-type-aliases@npm:0.0.0-5569" dependencies: "@docusaurus/react-loadable": 5.5.2 - "@docusaurus/types": 0.0.0-5566 + "@docusaurus/types": 0.0.0-5569 "@types/history": ^4.7.11 "@types/react": "*" "@types/react-router-config": "*" @@ -1776,19 +1776,19 @@ __metadata: peerDependencies: react: "*" react-dom: "*" - checksum: 6f8a86386c54829f41e28d01a7ffe96b2a3211aaa9fd217f614154726620ce118a0a9bba5563b512bdbc4d3e4a03e1d73848caadbd7fe2b55e59e4ff1ae3633a + checksum: 08168317c606e44f8ead775bfc23feaeb3a03178a19a07504130a32cbf189926c08ef40cdc4f00a6df0983383ca0145a42df9112451b15a19067d8b6b10d5f89 languageName: node linkType: hard -"@docusaurus/plugin-client-redirects@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/plugin-client-redirects@npm:0.0.0-5566" +"@docusaurus/plugin-client-redirects@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/plugin-client-redirects@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/logger": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 - "@docusaurus/utils-common": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/logger": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 + "@docusaurus/utils-common": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 eta: ^2.0.1 fs-extra: ^11.1.0 lodash: ^4.17.21 @@ -1796,21 +1796,21 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 01976bc176541b7506680f56f74f2f5cf8d128f33eb6151bdd3bf09795a5ec5161356f4d7711c1fb63e7617fbdd2c6d08a4243870144b04fc1c02b67a98bdcb6 + checksum: f082956089fc889ea67b89c5c19ad6cd0bc9bb35b663a701f05905786938b1302b8667c2b6d0354bcf9efd35496a5920e1871b488fbc6285d0e36cfe3187e84e languageName: node linkType: hard -"@docusaurus/plugin-content-blog@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/plugin-content-blog@npm:0.0.0-5566" +"@docusaurus/plugin-content-blog@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/plugin-content-blog@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/logger": 0.0.0-5566 - "@docusaurus/mdx-loader": 0.0.0-5566 - "@docusaurus/types": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 - "@docusaurus/utils-common": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/logger": 0.0.0-5569 + "@docusaurus/mdx-loader": 0.0.0-5569 + "@docusaurus/types": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 + "@docusaurus/utils-common": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 cheerio: ^1.0.0-rc.12 feed: ^4.2.2 fs-extra: ^11.1.0 @@ -1823,21 +1823,21 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 1d1dfaf61c98de3e5722d1ea726dae29b75eb8747600583347aaca1dfe1ba0cb3b023d1af02ab8aac6e8750288ecd7f5b90bde5e5a693a822c874f0c053a24ea + checksum: b9eef1200e1b9bf1cfa7c4d4cb62b7344306d3a096c0cbfc36afd4b973a940b1bf9d6dc62124a0028bbd466313c7ee219444ebe2856588748d2fc33d1f48ae80 languageName: node linkType: hard -"@docusaurus/plugin-content-docs@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/plugin-content-docs@npm:0.0.0-5566" +"@docusaurus/plugin-content-docs@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/plugin-content-docs@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/logger": 0.0.0-5566 - "@docusaurus/mdx-loader": 0.0.0-5566 - "@docusaurus/module-type-aliases": 0.0.0-5566 - "@docusaurus/types": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/logger": 0.0.0-5569 + "@docusaurus/mdx-loader": 0.0.0-5569 + "@docusaurus/module-type-aliases": 0.0.0-5569 + "@docusaurus/types": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 "@types/react-router-config": ^5.0.6 combine-promises: ^1.1.0 fs-extra: ^11.1.0 @@ -1850,133 +1850,133 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 32bb9c8c82ff4dfb9639b8fabda4d7b3c69c7a4a551719dba7495be8c7baf2ad96cb79b241904def5bab33f38c4e84a01d870f7200fe5c4eae74d380abaa6c60 + checksum: 544d51dbb6c9037916aa8e3ed493624029ffdac0d0da70cfdb74f1d431a8da071eeeb0d567be6f63d48f9922f1594f08f950478a4ea427fae8001ff0b4ac5d83 languageName: node linkType: hard -"@docusaurus/plugin-content-pages@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/plugin-content-pages@npm:0.0.0-5566" +"@docusaurus/plugin-content-pages@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/plugin-content-pages@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/mdx-loader": 0.0.0-5566 - "@docusaurus/types": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/mdx-loader": 0.0.0-5569 + "@docusaurus/types": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 fs-extra: ^11.1.0 tslib: ^2.5.0 webpack: ^5.76.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 6764c2d922d308305b0f71866f951b05a9cf885ff758e8982f865d8a52d77295802ae0d35cad20518b45a6a4daf7fbfbe2728fd5e078fdcc5104a4f21de0134f + checksum: 16fa680c069d57d9d299ef515487a773c5b89d605cfe5b843392a7bce5e59b539b1855d7e0f2e11878b3021038bff6468b5f4adb9af7dc06884d7207772c2f95 languageName: node linkType: hard -"@docusaurus/plugin-debug@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/plugin-debug@npm:0.0.0-5566" +"@docusaurus/plugin-debug@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/plugin-debug@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/types": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/types": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 fs-extra: ^11.1.0 react-json-view: ^1.21.3 tslib: ^2.5.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 32a996e40cc58ebc2fd3cbfb9f6d33b7303952788d9c9b2f41f36b54a327939807e990784191f6f3a6169a4c56c7dabec2f7b369828d86f86414e32c3ac39dfe + checksum: 0e3b8f5a92bc2b2ab193d76be6174b521238da7b4627775a852a4b4ff8690af28ab4586adbb84fc03abc6da276efc7474eed9350577b457e532defeb638d3184 languageName: node linkType: hard -"@docusaurus/plugin-google-analytics@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/plugin-google-analytics@npm:0.0.0-5566" +"@docusaurus/plugin-google-analytics@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/plugin-google-analytics@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/types": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/types": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 tslib: ^2.5.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: a1549324beb15024b3cfc6ebc0fb1ef7017f28d4a7419c14d469cc24025a53e460bceeed7401e01900b530e06ec84c302de891aea165ed5c3885261eb8eccb12 + checksum: a3e55d3a616009db499ca61493f9cf308c8ae012ec1ff680bff20c3b0b486f8d5d9552fd5c3d7b198e6c72d8e13ace7808d978bd8d56496953d2fd51d5ce0c32 languageName: node linkType: hard -"@docusaurus/plugin-google-gtag@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/plugin-google-gtag@npm:0.0.0-5566" +"@docusaurus/plugin-google-gtag@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/plugin-google-gtag@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/types": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/types": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 "@types/gtag.js": ^0.0.12 tslib: ^2.5.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 3e98f82b2e2767205fc3d40673e6db5b188d2cd4c3ff83d7f6ff7588b0e2486a6d48c1a6ef37bae6a1fafb77309bbc4000eb2784bd4f01df21deb23235f13884 + checksum: d0a7f58bea6d5bfd786120ab6c75ab10a4d67bae1d60aadedba649a516f2f9e49237a96a7764f964c79084ae500b9520761cbf6aab60bd5e43bb976f04dd3194 languageName: node linkType: hard -"@docusaurus/plugin-google-tag-manager@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/plugin-google-tag-manager@npm:0.0.0-5566" +"@docusaurus/plugin-google-tag-manager@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/plugin-google-tag-manager@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/types": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/types": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 tslib: ^2.5.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 6ea3e845ad5bab9cec34947e851e3adbf27c609227ad4dec8c4bad6ff05ab2190028dea78a4c0d844b6c377357189d9b739ca83c330d8f46dfe25e48ee61037a + checksum: acafad7eea5ba06c62addcef4f3cfaa6c43193827ec4b6c2d39ed2799df5301defa8ac8c73d49e35c328bd118c78412745f088764d97eac00da090be8e7906e5 languageName: node linkType: hard -"@docusaurus/plugin-sitemap@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/plugin-sitemap@npm:0.0.0-5566" +"@docusaurus/plugin-sitemap@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/plugin-sitemap@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/logger": 0.0.0-5566 - "@docusaurus/types": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 - "@docusaurus/utils-common": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/logger": 0.0.0-5569 + "@docusaurus/types": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 + "@docusaurus/utils-common": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 fs-extra: ^11.1.0 sitemap: ^7.1.1 tslib: ^2.5.0 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 1ceb243d048aaadfd05aaa4bb02455d3e87281ba49c08ecaa2ceb58f2cfa94a52af55756e20c562a2fa0c01524396d754d116e6ecb0d7591c2718baeaa334688 + checksum: 53fd5944a1cfbbe252e3f4398acf8aea3605ca3c5fb9ec48700ce6c162744bae541ccecd1d71e884777dfc2e98c6fa9a01b0f98829e6018e59003c3d4f4a1612 languageName: node linkType: hard -"@docusaurus/preset-classic@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/preset-classic@npm:0.0.0-5566" +"@docusaurus/preset-classic@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/preset-classic@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/plugin-content-blog": 0.0.0-5566 - "@docusaurus/plugin-content-docs": 0.0.0-5566 - "@docusaurus/plugin-content-pages": 0.0.0-5566 - "@docusaurus/plugin-debug": 0.0.0-5566 - "@docusaurus/plugin-google-analytics": 0.0.0-5566 - "@docusaurus/plugin-google-gtag": 0.0.0-5566 - "@docusaurus/plugin-google-tag-manager": 0.0.0-5566 - "@docusaurus/plugin-sitemap": 0.0.0-5566 - "@docusaurus/theme-classic": 0.0.0-5566 - "@docusaurus/theme-common": 0.0.0-5566 - "@docusaurus/theme-search-algolia": 0.0.0-5566 - "@docusaurus/types": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/plugin-content-blog": 0.0.0-5569 + "@docusaurus/plugin-content-docs": 0.0.0-5569 + "@docusaurus/plugin-content-pages": 0.0.0-5569 + "@docusaurus/plugin-debug": 0.0.0-5569 + "@docusaurus/plugin-google-analytics": 0.0.0-5569 + "@docusaurus/plugin-google-gtag": 0.0.0-5569 + "@docusaurus/plugin-google-tag-manager": 0.0.0-5569 + "@docusaurus/plugin-sitemap": 0.0.0-5569 + "@docusaurus/theme-classic": 0.0.0-5569 + "@docusaurus/theme-common": 0.0.0-5569 + "@docusaurus/theme-search-algolia": 0.0.0-5569 + "@docusaurus/types": 0.0.0-5569 peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: e7b1181b414f7795c540a4c9fed282ce350eaeee5006a9eb228cb42990d67ec933231f0af2f0329571ea135d87012a6ad606e9adaee61ae105573bbe6433a698 + checksum: a20013db1bd1c77e6fc2b901326d62f3003b6f6e6191f26aae6968cc2402837c0e84cc54571849d2af811fdae74d9359c0e96775d61b9f3efbbe881898e7f9a3 languageName: node linkType: hard @@ -1992,22 +1992,22 @@ __metadata: languageName: node linkType: hard -"@docusaurus/theme-classic@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/theme-classic@npm:0.0.0-5566" +"@docusaurus/theme-classic@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/theme-classic@npm:0.0.0-5569" dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/mdx-loader": 0.0.0-5566 - "@docusaurus/module-type-aliases": 0.0.0-5566 - "@docusaurus/plugin-content-blog": 0.0.0-5566 - "@docusaurus/plugin-content-docs": 0.0.0-5566 - "@docusaurus/plugin-content-pages": 0.0.0-5566 - "@docusaurus/theme-common": 0.0.0-5566 - "@docusaurus/theme-translations": 0.0.0-5566 - "@docusaurus/types": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 - "@docusaurus/utils-common": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/mdx-loader": 0.0.0-5569 + "@docusaurus/module-type-aliases": 0.0.0-5569 + "@docusaurus/plugin-content-blog": 0.0.0-5569 + "@docusaurus/plugin-content-docs": 0.0.0-5569 + "@docusaurus/plugin-content-pages": 0.0.0-5569 + "@docusaurus/theme-common": 0.0.0-5569 + "@docusaurus/theme-translations": 0.0.0-5569 + "@docusaurus/types": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 + "@docusaurus/utils-common": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 "@mdx-js/react": ^2.1.5 clsx: ^1.2.1 copy-text-to-clipboard: ^3.0.1 @@ -2024,21 +2024,21 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 17ba0e0d5f27b0dfd700dd1c9536eae416268abc8d4b308cdf4015b2aff246153b4906e5aa292be58987ccc10b9fe0d0d3182bbba52e370c3116076787009206 + checksum: ee25082a4abc0d812cc54ee1bea642766f903cfa47d28c6a6efbc53cdaacf67bee594a4745c7ae97fe3ba6ddb136de36dd0c8cda2c310c5b40c9023bee70fcbb languageName: node linkType: hard -"@docusaurus/theme-common@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/theme-common@npm:0.0.0-5566" +"@docusaurus/theme-common@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/theme-common@npm:0.0.0-5569" dependencies: - "@docusaurus/mdx-loader": 0.0.0-5566 - "@docusaurus/module-type-aliases": 0.0.0-5566 - "@docusaurus/plugin-content-blog": 0.0.0-5566 - "@docusaurus/plugin-content-docs": 0.0.0-5566 - "@docusaurus/plugin-content-pages": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 - "@docusaurus/utils-common": 0.0.0-5566 + "@docusaurus/mdx-loader": 0.0.0-5569 + "@docusaurus/module-type-aliases": 0.0.0-5569 + "@docusaurus/plugin-content-blog": 0.0.0-5569 + "@docusaurus/plugin-content-docs": 0.0.0-5569 + "@docusaurus/plugin-content-pages": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 + "@docusaurus/utils-common": 0.0.0-5569 "@types/history": ^4.7.11 "@types/react": "*" "@types/react-router-config": "*" @@ -2051,22 +2051,22 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 5f2978300b9a01c93d12e6fcd768259cab224207da15f6fd6889cc3e441fda61354e165f084f0353257e9c6d6af5184adb0f03ce7cf009845751657c3d52a00f + checksum: 26a2d7dd6fd8b97e67db3bc093fcb9ce6f14d561aeccf94c3334d18a9a2900483dcb0ec4bb28d31e910bea459f20e4445207d5156c9cc02bd72601eb0256e3cc languageName: node linkType: hard -"@docusaurus/theme-search-algolia@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/theme-search-algolia@npm:0.0.0-5566" +"@docusaurus/theme-search-algolia@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/theme-search-algolia@npm:0.0.0-5569" dependencies: "@docsearch/react": ^3.3.3 - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/logger": 0.0.0-5566 - "@docusaurus/plugin-content-docs": 0.0.0-5566 - "@docusaurus/theme-common": 0.0.0-5566 - "@docusaurus/theme-translations": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 - "@docusaurus/utils-validation": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/logger": 0.0.0-5569 + "@docusaurus/plugin-content-docs": 0.0.0-5569 + "@docusaurus/theme-common": 0.0.0-5569 + "@docusaurus/theme-translations": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 + "@docusaurus/utils-validation": 0.0.0-5569 algoliasearch: ^4.15.0 algoliasearch-helper: ^3.12.0 clsx: ^1.2.1 @@ -2078,23 +2078,23 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: 89256f242ccc5d0e1aca92e817413a1e40d18de58fb069e283741b2e5dbc29b267b345fc6d602ad88ac0faf0a003c3a99d549e9bc9027aa79b0e767ad4d7b79c + checksum: 16178b7b9278c91eaedbb1629192dc2255939de6f509b721305c4c1795283e0fd4f859b305ce051ec5efdbe71884ceede6239445916b60ebec1858e3b5161d8f languageName: node linkType: hard -"@docusaurus/theme-translations@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/theme-translations@npm:0.0.0-5566" +"@docusaurus/theme-translations@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/theme-translations@npm:0.0.0-5569" dependencies: fs-extra: ^11.1.0 tslib: ^2.5.0 - checksum: e6595e675617669267b48e8cf79cbde8e65f279b8cdf0a3a45cb75f32ec0d2ddd59d0d02ac3f0aaa5328f0a13cd63a41aaff49a2251ce6665bba4abdfba71361 + checksum: 695f94ddc66d54b649042f0fb0a88b692613d10898ee3306c6c9b52340373493ac9bcceec9263332454d7c9179339517e40b083e949510f9e48ec834bc61fbb3 languageName: node linkType: hard -"@docusaurus/types@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/types@npm:0.0.0-5566" +"@docusaurus/types@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/types@npm:0.0.0-5569" dependencies: "@types/history": ^4.7.11 "@types/react": "*" @@ -2107,13 +2107,13 @@ __metadata: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - checksum: aec430471a0019e650e168320c168f942ce8097022c45fb70e1ec86ba793b146f95a9d3cf434e3d9c985a45cea363f870cc51395dddaf0d0f600496ca6dbf810 + checksum: 956b7af3a75b3632d1ff330d018353b38b22b3f4af8b39f452fcde33d74c70312dadd3e3bb40e04ecbe02e2462a67bfcbd4ef631462a9c710616c83186930b69 languageName: node linkType: hard -"@docusaurus/utils-common@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/utils-common@npm:0.0.0-5566" +"@docusaurus/utils-common@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/utils-common@npm:0.0.0-5569" dependencies: tslib: ^2.5.0 peerDependencies: @@ -2121,28 +2121,28 @@ __metadata: peerDependenciesMeta: "@docusaurus/types": optional: true - checksum: 14cc02aac8a59350244ab733f8c662b838aac9ed3bc156a75113cbf1e2bf50d3db2e722313f9b6fe2616dcfafef3abc858f064289a8a0539b97745c571ec3fca + checksum: 7d98e31beea34be91de7fdc971033439e412f535617707c79c4d2c0653c769ff4ce7521bd87f038811ccbae3b1d5c0e5d26eab9935ee8bc3515b926cbe175365 languageName: node linkType: hard -"@docusaurus/utils-validation@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/utils-validation@npm:0.0.0-5566" +"@docusaurus/utils-validation@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/utils-validation@npm:0.0.0-5569" dependencies: - "@docusaurus/logger": 0.0.0-5566 - "@docusaurus/utils": 0.0.0-5566 + "@docusaurus/logger": 0.0.0-5569 + "@docusaurus/utils": 0.0.0-5569 joi: ^17.8.3 js-yaml: ^4.1.0 tslib: ^2.5.0 - checksum: 85a3570134ee26c88571ccb409c08f8ed182257cd72eb564e2a553e0d32a9a0c8bf4daaf1c33c5805e77e47a9fb97d29dfa0a758d8a7224d70ece80e82b583cc + checksum: b16730923e0d6fa627ca3e3105c152e814d540badb93f0a2ae1194672a76f8269ed48ed5c8f3259c0e0b9dfc6dc939035e0ac2e675b283ac4d414c642a95c893 languageName: node linkType: hard -"@docusaurus/utils@npm:0.0.0-5566": - version: 0.0.0-5566 - resolution: "@docusaurus/utils@npm:0.0.0-5566" +"@docusaurus/utils@npm:0.0.0-5569": + version: 0.0.0-5569 + resolution: "@docusaurus/utils@npm:0.0.0-5569" dependencies: - "@docusaurus/logger": 0.0.0-5566 + "@docusaurus/logger": 0.0.0-5569 "@svgr/webpack": ^6.5.1 escape-string-regexp: ^4.0.0 file-loader: ^6.2.0 @@ -2163,7 +2163,7 @@ __metadata: peerDependenciesMeta: "@docusaurus/types": optional: true - checksum: 82f682fdec2711d4608bb84f4a400e4f93bfbd4fde2da4cce37691b91e3e501bd409e7e86bae19bcc3b5dce576edcd33e078b25c28236ed1f15628d814cfe04e + checksum: 33e044f9b5328cf9d1b17637fa41a6138c5e18b5f3d73389bbb9f71735a273a9e27660d9a9103766406549edfa0691e2524cc84e6ebc2b0d308991df3bee0dd9 languageName: node linkType: hard @@ -3737,10 +3737,10 @@ __metadata: version: 0.0.0-use.local resolution: "backstage-microsite@workspace:." dependencies: - "@docusaurus/core": 0.0.0-5566 - "@docusaurus/module-type-aliases": 0.0.0-5566 - "@docusaurus/plugin-client-redirects": 0.0.0-5566 - "@docusaurus/preset-classic": 0.0.0-5566 + "@docusaurus/core": 0.0.0-5569 + "@docusaurus/module-type-aliases": 0.0.0-5569 + "@docusaurus/plugin-client-redirects": 0.0.0-5569 + "@docusaurus/preset-classic": 0.0.0-5569 "@spotify/prettier-config": ^14.0.0 "@swc/core": ^1.3.46 "@tsconfig/docusaurus": ^1.0.6 diff --git a/package.json b/package.json index 44778ae926..7e77896821 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@types/react": "^17", "@types/react-dom": "^17" }, - "version": "1.14.0-next.1", + "version": "1.14.0-next.2", "dependencies": { "@backstage/errors": "workspace:^", "@manypkg/get-packages": "^1.1.3" diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 5d781b58ca..a4ab5e013c 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/app-defaults +## 1.3.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + ## 1.3.1-next.1 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 0a8f2e5dc1..f1c806ebe2 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "1.3.1-next.1", + "version": "1.3.1-next.2", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index b20fafb995..84f7ab32a1 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,73 @@ # example-app +## 0.2.83-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-devtools@0.1.0-next.0 + - @backstage/plugin-kubernetes@0.9.0-next.2 + - @backstage/plugin-home@0.5.2-next.2 + - @backstage/theme@0.3.0-next.0 + - @backstage/plugin-badges@0.2.42-next.2 + - @backstage/plugin-tech-radar@0.6.4-next.2 + - @backstage/plugin-scaffolder-react@1.4.0-next.2 + - @backstage/plugin-explore@0.4.3-next.2 + - @backstage/plugin-org@0.6.8-next.2 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/plugin-azure-devops@0.3.0-next.2 + - @backstage/plugin-stack-overflow@0.1.15-next.2 + - @backstage/app-defaults@1.3.1-next.2 + - @backstage/cli@0.22.7-next.0 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-airbrake@0.3.18-next.2 + - @backstage/plugin-api-docs@0.9.3-next.2 + - @backstage/plugin-azure-sites@0.1.7-next.2 + - @backstage/plugin-catalog-graph@0.2.30-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/plugin-circleci@0.3.18-next.2 + - @backstage/plugin-cloudbuild@0.3.18-next.2 + - @backstage/plugin-code-coverage@0.2.11-next.2 + - @backstage/plugin-cost-insights@0.12.7-next.2 + - @backstage/plugin-dynatrace@5.0.0-next.2 + - @backstage/plugin-entity-feedback@0.2.1-next.2 + - @backstage/plugin-gcalendar@0.3.14-next.1 + - @backstage/plugin-gcp-projects@0.3.37-next.1 + - @backstage/plugin-github-actions@0.5.18-next.2 + - @backstage/plugin-gocd@0.1.24-next.2 + - @backstage/plugin-graphiql@0.2.50-next.1 + - @backstage/plugin-jenkins@0.7.17-next.2 + - @backstage/plugin-kafka@0.3.18-next.2 + - @backstage/plugin-lighthouse@0.4.3-next.2 + - @backstage/plugin-linguist@0.1.3-next.2 + - @backstage/plugin-microsoft-calendar@0.1.3-next.1 + - @backstage/plugin-newrelic@0.3.36-next.1 + - @backstage/plugin-octopus-deploy@0.2.0-next.2 + - @backstage/plugin-pagerduty@0.5.11-next.2 + - @backstage/plugin-playlist@0.1.9-next.2 + - @backstage/plugin-puppetdb@0.1.1-next.2 + - @backstage/plugin-rollbar@0.4.18-next.2 + - @backstage/plugin-scaffolder@1.13.1-next.2 + - @backstage/plugin-search@1.3.0-next.2 + - @backstage/plugin-sentry@0.5.3-next.2 + - @backstage/plugin-shortcuts@0.3.10-next.2 + - @backstage/plugin-stackstorm@0.1.2-next.1 + - @backstage/plugin-tech-insights@0.3.10-next.2 + - @backstage/plugin-techdocs@1.6.2-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.13-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/plugin-todo@0.2.20-next.2 + - @backstage/plugin-user-settings@0.7.3-next.2 + - @backstage/plugin-apache-airflow@0.2.11-next.1 + - @backstage/plugin-catalog-import@0.9.8-next.2 + - @backstage/plugin-newrelic-dashboard@0.2.11-next.2 + - @backstage/config@1.0.7 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + - @internal/plugin-catalog-customized@0.0.10-next.2 + ## 0.2.83-next.1 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index d4941ecd48..56ae2e9c9e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.83-next.1", + "version": "0.2.83-next.2", "private": true, "backstage": { "role": "frontend" @@ -29,6 +29,7 @@ "@backstage/plugin-cloudbuild": "workspace:^", "@backstage/plugin-code-coverage": "workspace:^", "@backstage/plugin-cost-insights": "workspace:^", + "@backstage/plugin-devtools": "workspace:^", "@backstage/plugin-dynatrace": "workspace:^", "@backstage/plugin-entity-feedback": "workspace:^", "@backstage/plugin-explore": "workspace:^", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 19edd9640d..bc9eac8c64 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -109,6 +109,8 @@ import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card'; import { StackstormPage } from '@backstage/plugin-stackstorm'; import { PuppetDbPage } from '@backstage/plugin-puppetdb'; +import { DevToolsPage } from '@backstage/plugin-devtools'; +import { customDevToolsPage } from './components/devtools/CustomDevToolsPage'; const app = createApp({ apis, @@ -291,6 +293,9 @@ const routes = ( } /> } /> } /> + }> + {customDevToolsPage} + ); diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 904fa4a7d4..ffaf72296a 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -52,6 +52,7 @@ import { MyGroupsSidebarItem } from '@backstage/plugin-org'; import { SearchModal } from '../search/SearchModal'; import Score from '@material-ui/icons/Score'; import { useApp } from '@backstage/core-plugin-api'; +import BuildIcon from '@material-ui/icons/Build'; const useSidebarLogoStyles = makeStyles({ root: { @@ -176,6 +177,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( to="/settings" > + {children} diff --git a/packages/app/src/components/devtools/CustomDevToolsPage.tsx b/packages/app/src/components/devtools/CustomDevToolsPage.tsx new file mode 100644 index 0000000000..dfe70426aa --- /dev/null +++ b/packages/app/src/components/devtools/CustomDevToolsPage.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ConfigContent, + ExternalDependenciesContent, + InfoContent, +} from '@backstage/plugin-devtools'; +import { DevToolsLayout } from '@backstage/plugin-devtools'; +import React from 'react'; + +const DevToolsPage = () => { + return ( + + + + + + + + + + + + ); +}; + +export const customDevToolsPage = ; diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx index 26a7ab60f3..da1d0d425c 100644 --- a/packages/app/src/components/home/HomePage.tsx +++ b/packages/app/src/components/home/HomePage.tsx @@ -21,12 +21,15 @@ import { ClockConfig, HomePageStarredEntities, CustomHomepageGrid, + HomePageToolkit, + HomePageCompanyLogo, } from '@backstage/plugin-home'; import { Content, Header, Page } from '@backstage/core-components'; import { HomePageSearchBar } from '@backstage/plugin-search'; import { HomePageCalendar } from '@backstage/plugin-gcalendar'; import { MicrosoftCalendarCard } from '@backstage/plugin-microsoft-calendar'; import React from 'react'; +import HomeIcon from '@material-ui/icons/Home'; const clockConfigs: ClockConfig[] = [ { @@ -55,12 +58,26 @@ const timeFormat: Intl.DateTimeFormatOptions = { const defaultConfig = [ { - component: 'HomePageSearchBar', + component: 'CompanyLogo', x: 0, y: 0, width: 12, height: 1, }, + { + component: 'WelcomeTitle', + x: 0, + y: 1, + width: 12, + height: 1, + }, + { + component: 'HomePageSearchBar', + x: 0, + y: 2, + width: 12, + height: 1, + }, ]; export const homePage = ( @@ -78,6 +95,17 @@ export const homePage = ( + + + , + }, + ]} + /> diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 045a385218..f8175bac29 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -46,10 +46,10 @@ "test:kubernetes": "backstage-cli package test -t KubernetesContainerRunner --no-watch" }, "dependencies": { - "@aws-sdk/abort-controller": "^3.208.0", - "@aws-sdk/client-s3": "^3.208.0", - "@aws-sdk/credential-providers": "^3.208.0", - "@aws-sdk/types": "^3.208.0", + "@aws-sdk/abort-controller": "^3.310.0", + "@aws-sdk/client-s3": "^3.310.0", + "@aws-sdk/credential-providers": "^3.310.0", + "@aws-sdk/types": "^3.310.0", "@backstage/backend-app-api": "workspace:^", "@backstage/backend-dev-utils": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", @@ -114,7 +114,7 @@ } }, "devDependencies": { - "@aws-sdk/util-stream-node": "^3.208.0", + "@aws-sdk/util-stream-node": "^3.310.0", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/archiver": "^5.1.0", diff --git a/packages/backend-next/CHANGELOG.md b/packages/backend-next/CHANGELOG.md index 2eea18b4af..e01a00f8d2 100644 --- a/packages/backend-next/CHANGELOG.md +++ b/packages/backend-next/CHANGELOG.md @@ -1,5 +1,15 @@ # example-backend-next +## 0.0.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/plugin-kubernetes-backend@0.11.0-next.2 + - @backstage/plugin-search-backend@1.3.1-next.2 + - @backstage/plugin-scaffolder-backend@1.13.2-next.2 + ## 0.0.11-next.1 ### Patch Changes diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index bf913492ac..f69435a838 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-next", - "version": "0.0.11-next.1", + "version": "0.0.11-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index 015a54cf45..2d3d5f021d 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/backend-openapi-utils +## 0.0.2-next.1 + +### Patch Changes + +- fe16bd39e83: Use permalinks for links including a line number reference +- 021cfbb5152: Corrected resolution of parameter nested schema to use central schemas. + ## 0.0.2-next.0 ### Patch Changes diff --git a/packages/backend-openapi-utils/README.md b/packages/backend-openapi-utils/README.md index fe22ac2cf2..ba286f94e8 100644 --- a/packages/backend-openapi-utils/README.md +++ b/packages/backend-openapi-utils/README.md @@ -8,7 +8,7 @@ This package is meant to provide a typed Express router for an OpenAPI spec. Bas ### Configuration -1. Run `yarn --cwd backstage-cli package schema:openapi:generate` to translate your `src/schema/openapi.yaml` to a new Typescript file in `src/schema/openapi.generated.ts`. The command will try to execute both a lint and prettier step on the generated file, where applicable. +1. Run `yarn --cwd backstage-cli package schema openapi generate` to translate your `src/schema/openapi.yaml` to a new Typescript file in `src/schema/openapi.generated.ts`. The command will try to execute both a lint and prettier step on the generated file, where applicable. 2. In your plugin's `src/service/createRouter.ts`, @@ -42,4 +42,4 @@ Router() as ApiRouter> ### Runtime validation -Using a package like [`express-openapi-validator`](https://www.npmjs.com/package/express-openapi-validator), would allow us to remove [validation of request bodies with `AJV`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/util.ts#L58). However, `AJV` currently doesn't have support for OpenAPI 3.1 and `express-openapi-validator` enforces full URL matching for paths, meaning it cannot be mounted at the router level. +Using a package like [`express-openapi-validator`](https://www.npmjs.com/package/express-openapi-validator), would allow us to remove [validation of request bodies with `AJV`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/catalog-backend/src/service/util.ts#L58). However, `AJV` currently doesn't have support for OpenAPI 3.1 and `express-openapi-validator` enforces full URL matching for paths, meaning it cannot be mounted at the router level. diff --git a/packages/backend-openapi-utils/api-report.md b/packages/backend-openapi-utils/api-report.md index 180a7846ed..a4db8ac7bd 100644 --- a/packages/backend-openapi-utils/api-report.md +++ b/packages/backend-openapi-utils/api-report.md @@ -333,7 +333,6 @@ declare namespace internal { ImmutableSchemaObject, DocParameter, DocParameters, - ResolveDocParameterSchema, ParameterSchema, MapToSchema, ParametersSchema, @@ -412,7 +411,7 @@ type OptionalMap< type ParameterSchema< Doc extends RequiredDoc, Schema extends ImmutableParameterObject['schema'], -> = ResolveDocParameterSchema extends infer R +> = SchemaRef extends infer R ? R extends ImmutableSchemaObject ? R extends JSONSchema7 ? FromSchema @@ -549,16 +548,6 @@ type RequiredMap< [P in Exclude, undefined>]: NonNullable; }; -// @public (undocumented) -type ResolveDocParameterSchema< - Doc extends RequiredDoc, - Schema extends ImmutableParameterObject['schema'], -> = Schema extends ImmutableReferenceObject - ? 'parameters' extends ComponentTypes - ? ComponentRef - : never - : Schema; - // @public (undocumented) type Response_2< Doc extends RequiredDoc, diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index e59da29840..b3926de712 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-openapi-utils", "description": "OpenAPI typescript support.", - "version": "0.0.2-next.0", + "version": "0.0.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend-openapi-utils/src/types/params.ts b/packages/backend-openapi-utils/src/types/params.ts index dce743e6e8..b737450db7 100644 --- a/packages/backend-openapi-utils/src/types/params.ts +++ b/packages/backend-openapi-utils/src/types/params.ts @@ -34,6 +34,7 @@ import { MapDiscriminatedUnion, PathTemplate, RequiredDoc, + SchemaRef, } from './common'; import { FromSchema, JSONSchema7 } from 'json-schema-to-ts'; @@ -76,25 +77,13 @@ export type DocParameters< } : never; -/** - * @public - */ -export type ResolveDocParameterSchema< - Doc extends RequiredDoc, - Schema extends ImmutableParameterObject['schema'], -> = Schema extends ImmutableReferenceObject - ? 'parameters' extends ComponentTypes - ? ComponentRef - : never - : Schema; - /** * @public */ export type ParameterSchema< Doc extends RequiredDoc, Schema extends ImmutableParameterObject['schema'], -> = ResolveDocParameterSchema extends infer R +> = SchemaRef extends infer R ? R extends ImmutableSchemaObject ? R extends JSONSchema7 ? FromSchema diff --git a/packages/backend-test-utils/README.md b/packages/backend-test-utils/README.md index d0f52388bb..f22cb1bd3c 100644 --- a/packages/backend-test-utils/README.md +++ b/packages/backend-test-utils/README.md @@ -12,6 +12,26 @@ cd plugins/my-plugin-backend yarn add --dev @backstage/backend-test-utils ``` +## Environment variables + +- `BACKSTAGE_TEST_DISABLE_DOCKER` + - Setting the value to `1` disables Docker for tests +- `CI` + - Setting the value to `1` enables long-running tests, including the ones utilizing Docker +- `BACKSTAGE_TEST_DOCKER_REGISTRY` + - Docker registry mirror address where to pull images for tests, for example `mycompany.docker.io/mirror` + - See [documentation](https://node.testcontainers.org/configuration/) for information + about authentication (`DOCKER_AUTH_CONFIG`) + +Connection strings for different databases that are used for testing. The value of the +string should point to the running instance of the database. + +- `BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING` +- `BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING` +- `BACKSTAGE_TEST_DATABASE_POSTGRES11_CONNECTION_STRING` +- `BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING` +- `BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING` + ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts index 89ec21e7cc..8c1d6f5193 100644 --- a/packages/backend-test-utils/src/database/types.ts +++ b/packages/backend-test-utils/src/database/types.ts @@ -16,6 +16,7 @@ import { DatabaseManager } from '@backstage/backend-common'; import { Knex } from 'knex'; +import { getDockerImageForName } from '../util/getDockerImageForName'; /** * The possible databases to test against. @@ -48,35 +49,35 @@ export const allDatabases: Record = POSTGRES_13: { name: 'Postgres 13.x', driver: 'pg', - dockerImageName: 'postgres:13', + dockerImageName: getDockerImageForName('postgres:13'), connectionStringEnvironmentVariableName: 'BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING', }, POSTGRES_12: { name: 'Postgres 12.x', driver: 'pg', - dockerImageName: 'postgres:12', + dockerImageName: getDockerImageForName('postgres:12'), connectionStringEnvironmentVariableName: 'BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING', }, POSTGRES_11: { name: 'Postgres 11.x', driver: 'pg', - dockerImageName: 'postgres:11', + dockerImageName: getDockerImageForName('postgres:11'), connectionStringEnvironmentVariableName: 'BACKSTAGE_TEST_DATABASE_POSTGRES11_CONNECTION_STRING', }, POSTGRES_9: { name: 'Postgres 9.x', driver: 'pg', - dockerImageName: 'postgres:9', + dockerImageName: getDockerImageForName('postgres:9'), connectionStringEnvironmentVariableName: 'BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING', }, MYSQL_8: { name: 'MySQL 8.x', driver: 'mysql2', - dockerImageName: 'mysql:8', + dockerImageName: getDockerImageForName('mysql:8'), connectionStringEnvironmentVariableName: 'BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING', }, diff --git a/packages/backend-test-utils/src/util/getDockerImageForName.ts b/packages/backend-test-utils/src/util/getDockerImageForName.ts new file mode 100644 index 0000000000..7869e7b418 --- /dev/null +++ b/packages/backend-test-utils/src/util/getDockerImageForName.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const getDockerImageForName = (name: string) => { + return process.env.BACKSTAGE_TEST_DOCKER_REGISTRY + ? `${process.env.BACKSTAGE_TEST_DOCKER_REGISTRY}/${name}` + : name; +}; diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index e676df6839..7a4031b654 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,22 @@ # example-backend +## 0.2.83-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-devtools-backend@0.1.0-next.0 + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/plugin-badges-backend@0.2.0-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.0-next.2 + - @backstage/plugin-kubernetes-backend@0.11.0-next.2 + - @backstage/plugin-auth-backend@0.18.3-next.2 + - @backstage/plugin-search-backend@1.3.1-next.2 + - example-app@0.2.83-next.2 + - @backstage/plugin-scaffolder-backend@1.13.2-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.14-next.2 + - @backstage/config@1.0.7 + ## 0.2.83-next.1 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 900ee150fc..b5a82fd611 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.83-next.1", + "version": "0.2.83-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,6 +42,7 @@ "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-code-coverage-backend": "workspace:^", + "@backstage/plugin-devtools-backend": "workspace:^", "@backstage/plugin-entity-feedback-backend": "workspace:^", "@backstage/plugin-events-backend": "workspace:^", "@backstage/plugin-events-node": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 593d30bc3b..976ad9a577 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -64,6 +64,7 @@ import playlist from './plugins/playlist'; import adr from './plugins/adr'; import lighthouse from './plugins/lighthouse'; import linguist from './plugins/linguist'; +import devTools from './plugins/devtools'; import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; @@ -158,8 +159,8 @@ async function main() { const eventsEnv = useHotMemoize(module, () => createEnv('events')); const exploreEnv = useHotMemoize(module, () => createEnv('explore')); const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse')); - const linguistEnv = useHotMemoize(module, () => createEnv('linguist')); + const devToolsEnv = useHotMemoize(module, () => createEnv('devtools')); const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); @@ -185,6 +186,7 @@ async function main() { apiRouter.use('/entity-feedback', await entityFeedback(entityFeedbackEnv)); apiRouter.use('/adr', await adr(adrEnv)); apiRouter.use('/linguist', await linguist(linguistEnv)); + apiRouter.use('/devtools', await devTools(devToolsEnv)); apiRouter.use(notFoundHandler()); await lighthouse(lighthouseEnv); diff --git a/packages/backend/src/plugins/badges.ts b/packages/backend/src/plugins/badges.ts index 37529689f3..fa50731809 100644 --- a/packages/backend/src/plugins/badges.ts +++ b/packages/backend/src/plugins/badges.ts @@ -28,5 +28,8 @@ export default async function createPlugin( config: env.config, discovery: env.discovery, badgeFactories: createDefaultBadgeFactories(), + tokenManager: env.tokenManager, + logger: env.logger, + identity: env.identity, }); } diff --git a/packages/backend/src/plugins/devtools.ts b/packages/backend/src/plugins/devtools.ts new file mode 100644 index 0000000000..8e1767ddb1 --- /dev/null +++ b/packages/backend/src/plugins/devtools.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createRouter } from '@backstage/plugin-devtools-backend'; +import { Router } from 'express'; +import type { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return createRouter({ + logger: env.logger, + config: env.config, + permissions: env.permissions, + }); +} diff --git a/packages/catalog-model/examples/domains/artists-domain.yaml b/packages/catalog-model/examples/domains/artists-domain.yaml index 2a74425791..0dc1b91cb5 100644 --- a/packages/catalog-model/examples/domains/artists-domain.yaml +++ b/packages/catalog-model/examples/domains/artists-domain.yaml @@ -3,6 +3,7 @@ kind: Domain metadata: name: artists description: Everything related to artists + title: Artists links: - url: http://example.com/domain/artists/ title: Domain Readme diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index bad849d283..39bee2ed05 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -58,6 +58,7 @@ Options: --lax --frontend --deprecated + --strict --config -h, --help ``` diff --git a/packages/cli/src/commands/config/validate.ts b/packages/cli/src/commands/config/validate.ts index c3d05291e0..c81d82832e 100644 --- a/packages/cli/src/commands/config/validate.ts +++ b/packages/cli/src/commands/config/validate.ts @@ -24,5 +24,6 @@ export default async (opts: OptionValues) => { mockEnv: opts.lax, fullVisibility: !opts.frontend, withDeprecatedKeys: opts.deprecated, + strict: opts.strict, }); }; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ba11a53cfd..3a40839e25 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -345,6 +345,10 @@ export function registerCommands(program: Command) { .option('--lax', 'Do not require environment variables to be set') .option('--frontend', 'Only validate the frontend configuration') .option('--deprecated', 'Output deprecated configuration settings') + .option( + '--strict', + 'Enable strict config validation, forbidding errors and unknown keys', + ) .option(...configOption) .description( 'Validate that the given configuration loads and matches schema', diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index e066a40e7a..00f7818b41 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -32,6 +32,30 @@ interface StartAppOptions { configPaths: string[]; } +function checkReactVersion() { + try { + // Make sure we're looking at the root of the target repo + const reactPkgPath = require.resolve('react/package.json', { + paths: [paths.targetRoot], + }); + const reactPkg = require(reactPkgPath); + if (reactPkg.version.startsWith('16.')) { + console.log( + chalk.yellow( + ` +⚠️ ⚠️ +⚠️ You are using React version 16, which is deprecated for use in Backstage. ⚠️ +⚠️ Please upgrade to React 17 by updating your packages/app dependencies. ⚠️ +⚠️ ⚠️ +`, + ), + ); + } + } catch { + /* ignored */ + } +} + export async function startFrontend(options: StartAppOptions) { if (options.verifyVersions) { const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); @@ -65,6 +89,8 @@ export async function startFrontend(options: StartAppOptions) { } } + checkReactVersion(); + const { name } = await fs.readJson(paths.resolveTarget('package.json')); const config = await loadCliConfig({ args: options.configPaths, diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index b34e1280c0..aaf3fcfcb9 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -32,6 +32,7 @@ type Options = { withFilteredKeys?: boolean; withDeprecatedKeys?: boolean; fullVisibility?: boolean; + strict?: boolean; }; export async function loadCliConfig(options: Options) { @@ -70,6 +71,7 @@ export async function loadCliConfig(options: Options) { dependencies: localPackageNames, // Include the package.json in the project root if it exists packagePaths: [paths.resolveTargetRoot('package.json')], + noUndeclaredProperties: options.strict, }); const { appConfigs } = await loadConfig({ @@ -93,6 +95,7 @@ export async function loadCliConfig(options: Options) { : ['frontend'], withFilteredKeys: options.withFilteredKeys, withDeprecatedKeys: options.withDeprecatedKeys, + ignoreSchemaErrors: !options.strict, }); const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs); diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index 1cc353fd4c..8cd469d4d7 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -180,14 +180,17 @@ export function loadConfigSchema( ): Promise; // @public -export type LoadConfigSchemaOptions = +export type LoadConfigSchemaOptions = ( | { dependencies: string[]; packagePaths?: string[]; } | { serialized: JsonObject; - }; + } +) & { + noUndeclaredProperties?: boolean; +}; // @public export function mergeConfigSchemas(schemas: JSONSchema7[]): JSONSchema7; diff --git a/packages/config-loader/src/schema/compile.ts b/packages/config-loader/src/schema/compile.ts index d193e7fbf2..f9e1a3bf76 100644 --- a/packages/config-loader/src/schema/compile.ts +++ b/packages/config-loader/src/schema/compile.ts @@ -25,6 +25,7 @@ import { CONFIG_VISIBILITIES, ConfigVisibility, } from './types'; +import { SchemaObject } from 'json-schema-traverse'; /** * This takes a collection of Backstage configuration schemas from various @@ -35,6 +36,9 @@ import { */ export function compileConfigSchemas( schemas: ConfigSchemaPackageEntry[], + options?: { + noUndeclaredProperties?: boolean; + }, ): ValidationFunc { // The ajv instance below is stateful and doesn't really allow for additional // output during validation. We work around this by having this extra piece @@ -102,6 +106,18 @@ export function compileConfigSchemas( const merged = mergeConfigSchemas(schemas.map(_ => _.value)); + if (options?.noUndeclaredProperties) { + traverse(merged, (schema: SchemaObject) => { + /** + * The `additionalProperties` key can only be applied to `type: object` in the JSON + * schema. + */ + if (schema?.type === 'object') { + schema.additionalProperties ||= false; + } + }); + } + const validate = ajv.compile(merged); const visibilityBySchemaPath = new Map(); diff --git a/packages/config-loader/src/schema/load.ts b/packages/config-loader/src/schema/load.ts index 0309f64ea0..1a96305ae0 100644 --- a/packages/config-loader/src/schema/load.ts +++ b/packages/config-loader/src/schema/load.ts @@ -32,12 +32,16 @@ import { * @public */ export type LoadConfigSchemaOptions = - | { - dependencies: string[]; - packagePaths?: string[]; - } - | { - serialized: JsonObject; + | ( + | { + dependencies: string[]; + packagePaths?: string[]; + } + | { + serialized: JsonObject; + } + ) & { + noUndeclaredProperties?: boolean; }; function errorsToError(errors: ValidationError[]): Error { @@ -77,7 +81,9 @@ export async function loadConfigSchema( schemas = serialized.schemas as ConfigSchemaPackageEntry[]; } - const validate = compileConfigSchemas(schemas); + const validate = compileConfigSchemas(schemas, { + noUndeclaredProperties: options.noUndeclaredProperties, + }); return { process( diff --git a/packages/config-loader/src/sources/RemoteConfigSource.ts b/packages/config-loader/src/sources/RemoteConfigSource.ts index ff75060f56..3a8f6bf812 100644 --- a/packages/config-loader/src/sources/RemoteConfigSource.ts +++ b/packages/config-loader/src/sources/RemoteConfigSource.ts @@ -149,7 +149,7 @@ export class RemoteConfigSource implements ConfigSource { signal: signal as import('node-fetch').RequestInit['signal'], }); if (!res.ok) { - throw ResponseError.fromResponse(res); + throw await ResponseError.fromResponse(res); } const content = await res.text(); diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts index c71799ed98..1c51dd0698 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts @@ -79,6 +79,16 @@ describe('MicrosoftAuth', () => { 'Requested access token with scopes from multiple Azure resources: one-resource, other-resource. Access tokens can only have a single audience.', ); }); + + it('succeeds when requesting multiple scopes for the same resource', async () => { + const accessTokenPromise = microsoftAuth.getAccessToken( + 'same-resource/one-scope same-resource/other-scope', + ); + + await expect(accessTokenPromise).resolves.toEqual( + 'tokenForOtherResource', + ); + }); }); describe('without a refresh token', () => { diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index fdffb3fc45..c57c65d830 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -91,10 +91,15 @@ export default class MicrosoftAuth { } private static resourceForScopes(scope: string): Promise { - const audiences = scope - .split(' ') - .map(MicrosoftAuth.resourceForScope) - .filter(aud => aud !== 'openid'); + const audiences = [ + ...new Set( + scope + .split(' ') + .map(MicrosoftAuth.resourceForScope) + .filter(aud => aud !== 'openid'), + ), + ]; + if (audiences.length > 1) { return Promise.reject( new Error( diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 825b793d2c..90f4e174b0 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/core-components +## 0.13.1-next.1 + +### Patch Changes + +- 83b45f9df50: Fix accessibility issue with Backstage Table's header style +- e97769f7c0b: Fix accessibility issue on controlled select input on tab navigation + keyboard enter/space action. +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.13.1-next.0 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index cdb5a6e4a8..ec46c4c6e3 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.13.1-next.0", + "version": "0.13.1-next.1", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/core-components/src/components/Link/Link.tsx b/packages/core-components/src/components/Link/Link.tsx index be91753ea3..4d58fdfa77 100644 --- a/packages/core-components/src/components/Link/Link.tsx +++ b/packages/core-components/src/components/Link/Link.tsx @@ -188,6 +188,9 @@ export const Link = React.forwardRef( ', () => { expect(input.textContent).toBe('All results'); // Simulate click on input - fireEvent.click(input); + fireEvent.mouseDown(within(input).getByRole('button')); expect(getByText('test 1')).toBeInTheDocument(); const option = getByText('test 1'); diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx index 1b57e0d4da..dbf5601112 100644 --- a/packages/core-components/src/components/Select/Select.tsx +++ b/packages/core-components/src/components/Select/Select.tsx @@ -16,7 +16,6 @@ import Box from '@material-ui/core/Box'; import Checkbox from '@material-ui/core/Checkbox'; import Chip from '@material-ui/core/Chip'; -import ClickAwayListener from '@material-ui/core/ClickAwayListener'; import FormControl from '@material-ui/core/FormControl'; import InputBase from '@material-ui/core/InputBase'; import InputLabel from '@material-ui/core/InputLabel'; @@ -171,7 +170,7 @@ export function SelectComponent(props: SelectProps) { onChange(event.target.value as SelectedItems); }; - const handleClick = (event: React.ChangeEvent) => { + const handleOpen = (event: React.ChangeEvent) => { if (disabled) { event.preventDefault(); return; @@ -184,7 +183,7 @@ export function SelectComponent(props: SelectProps) { }); }; - const handleClickAway = () => { + const handleClose = () => { setOpen(false); }; @@ -196,88 +195,85 @@ export function SelectComponent(props: SelectProps) { return ( - - - {label} - } + label={label} + tabIndex={0} + renderValue={s => + multiple && (value as any[]).length !== 0 ? ( + + {(s as string[]).map(selectedValue => ( + el.value === selectedValue)?.value} + label={items.find(el => el.value === selectedValue)?.label} + clickable + onDelete={handleDelete(selectedValue)} + className={classes.chip} + /> ))} - - - + + ) : ( + + {(value as any[]).length === 0 + ? placeholder || '' + : items.find(el => el.value === s)?.label} + + ) + } + IconComponent={() => + !isOpen ? : + } + MenuProps={{ + anchorOrigin: { + vertical: 'bottom', + horizontal: 'left', + }, + transformOrigin: { + vertical: 'top', + horizontal: 'left', + }, + getContentAnchorEl: null, + }} + > + {placeholder && !multiple && ( + {placeholder} + )} + {native + ? items && + items.map(item => ( + + )) + : items && + items.map(item => ( + + {multiple && ( + + )} + {item.label} + + ))} + + ); } diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx index 848c85eb60..5cea3dd6b6 100644 --- a/packages/core-components/src/components/Table/Table.tsx +++ b/packages/core-components/src/components/Table/Table.tsx @@ -105,7 +105,6 @@ const StyledMTableHeader = withStyles( borderTop: `1px solid ${theme.palette.grey.A100}`, borderBottom: `1px solid ${theme.palette.grey.A100}`, // withStyles hasn't a generic overload for theme - color: (theme as BackstageTheme).palette.textSubtle, fontWeight: theme.typography.fontWeightBold, position: 'static', wordBreak: 'normal', diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 948c4017a3..16d93b1867 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/create-app +## 0.5.1-next.2 + +### Patch Changes + +- 1d5e42655cd: Correct command to create new plugins + ## 0.5.1-next.1 ### Patch Changes @@ -2595,7 +2601,7 @@ fields that clarify a previous grey area such that the intent of the alteration is made explicit. This is caught by `tsc` and your editor if you are using the `@ts-check` and `@param` syntax in your migration files - ([example](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)), + ([example](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)), which we strongly recommend. See the [`knex` documentation](https://knexjs.org/#Schema-alter) for more @@ -2665,7 +2671,7 @@ fields that clarify a previous grey area such that the intent of the alteration is made explicit. This is caught by `tsc` and your editor if you are using the `@ts-check` and `@param` syntax in your migration files - ([example](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)), + ([example](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)), which we strongly recommend. See the [`knex` documentation](https://knexjs.org/#Schema-alter) for more diff --git a/packages/create-app/package.json b/packages/create-app/package.json index ef61e1c5b8..841c1b401d 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.5.1-next.1", + "version": "0.5.1-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index f00500af68..0682114a35 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -39,10 +39,7 @@ }, "resolutions": { "@types/react": "^17", - "@types/react-dom": "^17", - "@octokit/rest": "18.5.6", - "@octokit/plugin-rest-endpoint-methods": "5.16.2", - "@octokit/plugin-paginate-rest": "2.7.0" + "@types/react-dom": "^17" }, "prettier": "@spotify/prettier-config", "lint-staged": { diff --git a/packages/create-app/templates/default-app/plugins/README.md b/packages/create-app/templates/default-app/plugins/README.md index 58dc32cb08..d7865fdba3 100644 --- a/packages/create-app/templates/default-app/plugins/README.md +++ b/packages/create-app/templates/default-app/plugins/README.md @@ -4,6 +4,6 @@ This is where your own plugins and their associated modules live, each in a separate folder of its own. If you want to create a new plugin here, go to your project root directory, run -the command `yarn backstage-cli create`, and follow the on-screen instructions. +the command `yarn new`, and follow the on-screen instructions. You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)! diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index a6aeda0901..77e5241e7e 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/dev-utils +## 1.0.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/app-defaults@1.3.1-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/test-utils@1.3.1-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 1.0.15-next.1 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index ca5057c928..c14732070f 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "1.0.15-next.1", + "version": "1.0.15-next.2", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index 2486a83f5c..eaec405ec9 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,12 @@ # e2e-test +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.1-next.2 + ## 0.2.3-next.1 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 513a9c6a96..60877e8d66 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.2.3-next.1", + "version": "0.2.3-next.2", "private": true, "backstage": { "role": "cli" diff --git a/packages/integration-aws-node/package.json b/packages/integration-aws-node/package.json index 7afc31cc47..30a95b549e 100644 --- a/packages/integration-aws-node/package.json +++ b/packages/integration-aws-node/package.json @@ -32,11 +32,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@aws-sdk/client-sts": "^3.208.0", - "@aws-sdk/credential-provider-node": "^3.208.0", - "@aws-sdk/credential-providers": "^3.208.0", - "@aws-sdk/types": "^3.208.0", - "@aws-sdk/util-arn-parser": "^3.208.0", + "@aws-sdk/client-sts": "^3.310.0", + "@aws-sdk/credential-provider-node": "^3.310.0", + "@aws-sdk/credential-providers": "^3.310.0", + "@aws-sdk/types": "^3.310.0", + "@aws-sdk/util-arn-parser": "^3.310.0", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^" }, diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index ac62e9aab7..f9c035b7a6 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/integration-react +## 1.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 1.1.13-next.1 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index e59c8f48c4..3124557ebc 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "1.1.13-next.1", + "version": "1.1.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index b361bac02f..789b4bc407 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -14,8 +14,7 @@ Options: Commands: api-reports [options] [paths...] type-deps - schema:openapi:verify [paths...] - schema:openapi:generate [paths...] + schema [command] help [command] ``` @@ -37,19 +36,46 @@ Options: -h, --help ``` -### `backstage-repo-tools schema:openapi:generate` +### `backstage-repo-tools schema` ``` -Usage: backstage-repo-tools schema:openapi:generate [options] [paths...] +Usage: backstage-repo-tools schema [options] [command] [command] + +Options: + -h, --help + +Commands: + openapi [command] + help [command] +``` + +### `backstage-repo-tools schema openapi` + +``` +Usage: backstage-repo-tools schema openapi [options] [command] [command] + +Options: + -h, --help + +Commands: + verify [paths...] + generate [paths...] + help [command] +``` + +### `backstage-repo-tools schema openapi generate` + +``` +Usage: backstage-repo-tools schema openapi generate [options] [paths...] Options: -h, --help ``` -### `backstage-repo-tools schema:openapi:verify` +### `backstage-repo-tools schema openapi verify` ``` -Usage: backstage-repo-tools schema:openapi:verify [options] [paths...] +Usage: backstage-repo-tools schema openapi verify [options] [paths...] Options: -h, --help diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index f4d02df3bc..a8324c7d33 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -18,6 +18,30 @@ import { assertError } from '@backstage/errors'; import { Command } from 'commander'; import { exitWithError } from '../lib/errors'; +function registerSchemaCommand(program: Command) { + const command = program + .command('schema [command]') + .description('Various tools for working with API schema'); + + const openApiCommand = command + .command('openapi [command]') + .description('Tooling for OpenApi schema'); + + openApiCommand + .command('verify [paths...]') + .description( + 'Verify that all OpenAPI schemas are valid and have a matching `schemas/openapi.generated.ts` file.', + ) + .action(lazy(() => import('./openapi/verify').then(m => m.bulkCommand))); + + openApiCommand + .command('generate [paths...]') + .description( + 'Generates a Typescript file from an OpenAPI yaml spec. For use with the `@backstage/backend-openapi-utils` ApiRouter type.', + ) + .action(lazy(() => import('./openapi/generate').then(m => m.bulkCommand))); +} + export function registerCommands(program: Command) { program .command('api-reports [paths...]') @@ -63,19 +87,7 @@ export function registerCommands(program: Command) { .description('Find inconsistencies in types of all packages and plugins') .action(lazy(() => import('./type-deps/type-deps').then(m => m.default))); - program - .command('schema:openapi:verify [paths...]') - .description( - 'Verify that all OpenAPI schemas are valid and have a matching `schemas/openapi.generated.ts` file.', - ) - .action(lazy(() => import('./openapi/verify').then(m => m.bulkCommand))); - - program - .command('schema:openapi:generate [paths...]') - .description( - 'Generates a Typescript file from an OpenAPI yaml spec. For use with the `@backstage/backend-openapi-utils` ApiRouter type.', - ) - .action(lazy(() => import('./openapi/generate').then(m => m.bulkCommand))); + registerSchemaCommand(program); } // Wraps an action function so that it always exits and handles errors diff --git a/packages/repo-tools/src/commands/openapi/verify.ts b/packages/repo-tools/src/commands/openapi/verify.ts index f6dda66738..4c4f310f73 100644 --- a/packages/repo-tools/src/commands/openapi/verify.ts +++ b/packages/repo-tools/src/commands/openapi/verify.ts @@ -47,7 +47,7 @@ async function verify(directoryPath: string) { if (!isEqual(schema.default, yaml)) { const path = relativePath(cliPaths.targetRoot, directoryPath); throw new Error( - `\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools schema:openapi:generate ${path}\` to regenerate \`${TS_SCHEMA_PATH}\`.`, + `\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools schema openapi generate ${path}\` to regenerate \`${TS_SCHEMA_PATH}\`.`, ); } } diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 98bf3668d6..3e7e890e7c 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,23 @@ # techdocs-cli-embedded-app +## 0.2.82-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/app-defaults@1.3.1-next.2 + - @backstage/cli@0.22.7-next.0 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/test-utils@1.3.1-next.2 + - @backstage/plugin-catalog@1.11.0-next.2 + - @backstage/plugin-techdocs@1.6.2-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/config@1.0.7 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.82-next.1 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 01f5f8128c..a645d2dac1 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.82-next.1", + "version": "0.2.82-next.2", "private": true, "backstage": { "role": "frontend" diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 9fd73a6840..6804a09762 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/test-utils +## 1.3.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/config@1.0.7 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + ## 1.3.1-next.1 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 80d5a2e620..fa2557c4de 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "1.3.1-next.1", + "version": "1.3.1-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index e2490f346e..1e676394b5 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/theme +## 0.3.0-next.0 + +### Minor Changes + +- 98c0c199b15: Updates light theme's primary foreground and `running` status indicator colours to meet WCAG. Previously #2E77D0 changed to #1F5493. + +### Patch Changes + +- 83b45f9df50: Fix accessibility issue with Backstage Table's header style + ## 0.2.19 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index e102c067d7..f0e26f5739 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.2.19", + "version": "0.3.0-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 53f3a3ef2b..c0a9022bd0 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -159,7 +159,6 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides { head: { wordBreak: 'break-word', overflow: 'hidden', - color: theme.palette.textSubtle, fontWeight: 'normal', lineHeight: '1', }, @@ -198,14 +197,10 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides { '&:hover': { color: 'inherit', }, - '&:focus': { - color: 'inherit', - }, }, // Bold font for highlighting selected column active: { fontWeight: 'bold', - color: 'inherit', }, }, MuiListItemText: { diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index eb2ef7ce41..23438a1c14 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -33,7 +33,7 @@ export const lightTheme = createTheme({ ok: '#1DB954', warning: '#FF9800', error: '#E22134', - running: '#2E77D0', + running: '#1F5493', pending: '#FFED51', aborted: '#757575', }, @@ -48,7 +48,7 @@ export const lightTheme = createTheme({ }, }, primary: { - main: '#2E77D0', + main: '#1F5493', }, banner: { info: '#2E77D0', diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md index b0ce02fa19..ce583e56d2 100644 --- a/plugins/adr/CHANGELOG.md +++ b/plugins/adr/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-adr +## 0.5.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.5.1-next.1 ### Patch Changes diff --git a/plugins/adr/package.json b/plugins/adr/package.json index 3202a8c14e..707f0396cf 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr", - "version": "0.5.1-next.1", + "version": "0.5.1-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx index d136de4717..da575d884f 100644 --- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -29,7 +29,7 @@ import { SupportButton, WarningPanel, } from '@backstage/core-components'; -import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { AdrFilePathFilterFn, @@ -170,6 +170,9 @@ export const EntityAdrContent = (props: { const adrApi = useApi(adrApiRef); const entityHasAdrs = isAdrAvailable(entity); + const config = useApi(configApiRef); + const appSupportConfigured = config?.getOptionalConfig('app.support'); + const { value, loading, error } = useAsync(async () => { const url = getAdrLocationUrl(entity, scmIntegrations); return adrApi.listAdrs(url); @@ -212,7 +215,7 @@ export const EntityAdrContent = (props: { return ( - + {appSupportConfigured && } {!entityHasAdrs && ( diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index 191207e591..15472a973a 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-airbrake +## 0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/dev-utils@1.0.15-next.2 + - @backstage/test-utils@1.3.1-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.18-next.1 ### Patch Changes diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 6f7f404742..3934c29680 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.3.18-next.1", + "version": "0.3.18-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index a20bd80961..e656638fd6 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-allure +## 0.1.34-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.34-next.1 ### Patch Changes diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 2a98ae1faf..d8fb27f36c 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-allure", "description": "A Backstage plugin that integrates with Allure", - "version": "0.1.34-next.1", + "version": "0.1.34-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index 4195e02248..ec3122845a 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-analytics-module-ga +## 0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.29-next.0 ### Patch Changes diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 31215bd84f..2b2542ec2d 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga", - "version": "0.1.29-next.0", + "version": "0.1.29-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/analytics-module-ga4/CHANGELOG.md b/plugins/analytics-module-ga4/CHANGELOG.md index 59c03fec0f..15e67dd797 100644 --- a/plugins/analytics-module-ga4/CHANGELOG.md +++ b/plugins/analytics-module-ga4/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-analytics-module-ga4 +## 0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.0-next.1 ### Patch Changes diff --git a/plugins/analytics-module-ga4/package.json b/plugins/analytics-module-ga4/package.json index 5e27228534..eee914df92 100644 --- a/plugins/analytics-module-ga4/package.json +++ b/plugins/analytics-module-ga4/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga4", - "version": "0.1.0-next.1", + "version": "0.1.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index 368995a369..612a2a3e1d 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-apache-airflow +## 0.2.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.11-next.0 ### Patch Changes diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index 6d503e972a..cf7cf9553c 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.2.11-next.0", + "version": "0.2.11-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 635b1e2dca..f370ab46ee 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-api-docs +## 0.9.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog@1.11.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.9.3-next.1 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 89e65dfcc3..e31e2ec48e 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.9.3-next.1", + "version": "0.9.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md index 174a862d61..38d322a270 100644 --- a/plugins/apollo-explorer/CHANGELOG.md +++ b/plugins/apollo-explorer/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-apollo-explorer +## 0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.11-next.0 ### Patch Changes diff --git a/plugins/apollo-explorer/README.md b/plugins/apollo-explorer/README.md index 98f3116966..23ea71e934 100644 --- a/plugins/apollo-explorer/README.md +++ b/plugins/apollo-explorer/README.md @@ -38,17 +38,22 @@ yarn --cwd packages/app add @backstage/plugin-apollo-explorer Then, in `packages/app/src/App.tsx` add the plugin as a `Route` ```typescript - + {/* other routes... */} + + } /> - } -/> ``` Then, in `packages/app/src/components/Root/Root.tsx` add a sidebar item so users can find your beautiful plugin! diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json index c1164cf1d6..5117fa632c 100644 --- a/plugins/apollo-explorer/package.json +++ b/plugins/apollo-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apollo-explorer", - "version": "0.1.11-next.0", + "version": "0.1.11-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index c6ff454a7d..406298f6d2 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend +## 0.18.3-next.2 + +### Patch Changes + +- 3ffcdac7d07: Added a persistent session store through the database +- Updated dependencies + - @backstage/config@1.0.7 + ## 0.18.3-next.1 ### Patch Changes diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 8d76ca8bf5..c6579e08f8 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -59,6 +59,7 @@ export interface Config { /** * The available auth-provider options and attributes + * @additionalProperties true */ providers?: { google?: { diff --git a/plugins/auth-backend/migrations/20230428155633_sessions.js b/plugins/auth-backend/migrations/20230428155633_sessions.js new file mode 100644 index 0000000000..49462ac503 --- /dev/null +++ b/plugins/auth-backend/migrations/20230428155633_sessions.js @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + // See https://github.com/gx0r/connect-session-knex + // Modeled loosely after https://github.com/gx0r/connect-session-knex/blob/4e0e36a9afbb13c3000a89f5e341f2d2d4339a02/lib/index.js#L114 + // For simplicity we always make the session a string + // Do NOT change around this table or column names; the connect-session-knex library makes assumptions about them + await knex.schema.createTable('sessions', table => { + table.comment('Session data'); + table.string('sid').primary().notNullable().comment('ID of the session'); + table + .text('sess', 'longtext') + .notNullable() + .comment('Session data, JSON serialized'); + table + .timestamp('expired') + .notNullable() + .comment('The point in time when the session expires'); + table.index('sid', 'sessions_sid_idx'); + table.index('expired', 'sessions_expired_idx'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('sessions', table => { + table.dropIndex([], 'sessions_sid_idx'); + table.dropIndex([], 'sessions_expired_idx'); + }); + await knex.schema.dropTable('sessions'); +}; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 61c3fe9a8d..fbaf3c5423 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.18.3-next.1", + "version": "0.18.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -44,6 +44,7 @@ "@types/express": "^4.17.6", "@types/passport": "^1.0.3", "compression": "^1.7.4", + "connect-session-knex": "^3.0.1", "cookie-parser": "^1.4.5", "cors": "^2.8.5", "express": "^4.17.1", diff --git a/plugins/auth-backend/src/database/AuthDatabase.ts b/plugins/auth-backend/src/database/AuthDatabase.ts new file mode 100644 index 0000000000..97e4236754 --- /dev/null +++ b/plugins/auth-backend/src/database/AuthDatabase.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DatabaseManager, + PluginDatabaseManager, + resolvePackagePath, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { Knex } from 'knex'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', +); + +/** + * Ensures that a database connection is established exactly once and only when + * asked for, and runs migrations. + */ +export class AuthDatabase { + readonly #database: PluginDatabaseManager; + #promise: Promise | undefined; + + static create(database: PluginDatabaseManager): AuthDatabase { + return new AuthDatabase(database); + } + + /** @internal */ + static forTesting(): AuthDatabase { + const config = new ConfigReader({ + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + useNullAsDefault: true, + }, + }, + }); + const database = DatabaseManager.fromConfig(config).forPlugin('auth'); + return new AuthDatabase(database); + } + + static async runMigrations(knex: Knex): Promise { + await knex.migrate.latest({ + directory: migrationsDir, + }); + } + + private constructor(database: PluginDatabaseManager) { + this.#database = database; + } + + get(): Promise { + this.#promise ??= this.#database.getClient().then(async client => { + if (!this.#database.migrations?.skip) { + await AuthDatabase.runMigrations(client); + } + return client; + }); + + return this.#promise; + } +} diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts index da67a2f88f..cf415c7e13 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts @@ -14,33 +14,10 @@ * limitations under the License. */ -import Knex, { Knex as KnexType } from 'knex'; -import { DatabaseKeyStore } from './DatabaseKeyStore'; import { DateTime } from 'luxon'; - -function createDatabaseManager( - client: KnexType, - skipMigrations: boolean = false, -) { - return { - getClient: async () => client, - migrations: { - skip: skipMigrations, - }, - }; -} - -function createDB() { - const knex = Knex({ - client: 'better-sqlite3', - connection: ':memory:', - useNullAsDefault: true, - }); - knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => { - resource.run('PRAGMA foreign_keys = ON', () => {}); - }); - return knex; -} +import { AuthDatabase } from '../database/AuthDatabase'; +import { DatabaseKeyStore } from './DatabaseKeyStore'; +import { TestDatabases } from '@backstage/backend-test-utils'; const keyBase = { use: 'sig', @@ -48,95 +25,107 @@ const keyBase = { alg: 'Base64', } as const; +jest.setTimeout(60_000); + describe('DatabaseKeyStore', () => { - it('should store a key', async () => { - const client = createDB(); - const store = await DatabaseKeyStore.create({ - database: createDatabaseManager(client), - }); - - const key = { - kid: '123', - ...keyBase, - }; - - await expect(store.listKeys()).resolves.toEqual({ items: [] }); - await store.addKey(key); - - const { items } = await store.listKeys(); - expect(items).toEqual([{ createdAt: expect.anything(), key }]); - expect( - Math.abs( - DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds, - ), - ).toBeLessThan(10); + const databases = TestDatabases.create({ + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); - it('should remove stored keys', async () => { - const client = createDB(); - const store = await DatabaseKeyStore.create({ - database: createDatabaseManager(client), - }); + it.each(databases.eachSupportedId())( + 'should store a key, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await AuthDatabase.runMigrations(knex); - const key1 = { kid: '1', ...keyBase }; - const key2 = { kid: '2', ...keyBase }; - const key3 = { kid: '3', ...keyBase }; + const store = new DatabaseKeyStore(knex); - await store.addKey(key1); - await store.addKey(key2); - await store.addKey(key3); + const key = { + kid: '123', + ...keyBase, + }; - await expect(store.listKeys()).resolves.toEqual({ - items: [ - { key: key1, createdAt: expect.anything() }, - { key: key2, createdAt: expect.anything() }, - { key: key3, createdAt: expect.anything() }, - ], - }); + await expect(store.listKeys()).resolves.toEqual({ items: [] }); + await store.addKey(key); - store.removeKeys(['1']); + const { items } = await store.listKeys(); + expect(items).toEqual([{ createdAt: expect.anything(), key }]); + expect( + Math.abs( + DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds, + ), + ).toBeLessThan(10); + }, + ); - await expect(store.listKeys()).resolves.toEqual({ - items: [ - { key: key2, createdAt: expect.anything() }, - { key: key3, createdAt: expect.anything() }, - ], - }); + it.each(databases.eachSupportedId())( + 'should remove stored keys, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await AuthDatabase.runMigrations(knex); - store.removeKeys(['1', '2']); + const store = new DatabaseKeyStore(knex); - await expect(store.listKeys()).resolves.toEqual({ - items: [{ key: key3, createdAt: expect.anything() }], - }); + const key1 = { kid: '1', ...keyBase }; + const key2 = { kid: '2', ...keyBase }; + const key3 = { kid: '3', ...keyBase }; - store.removeKeys([]); + await store.addKey(key1); + await store.addKey(key2); + await store.addKey(key3); - await expect(store.listKeys()).resolves.toEqual({ - items: [{ key: key3, createdAt: expect.anything() }], - }); + await expect(store.listKeys()).resolves.toEqual({ + items: [ + { key: key1, createdAt: expect.anything() }, + { key: key2, createdAt: expect.anything() }, + { key: key3, createdAt: expect.anything() }, + ], + }); - store.removeKeys(['3', '4']); + await store.removeKeys(['1']); - await expect(store.listKeys()).resolves.toEqual({ - items: [], - }); + await expect(store.listKeys()).resolves.toEqual({ + items: [ + { key: key2, createdAt: expect.anything() }, + { key: key3, createdAt: expect.anything() }, + ], + }); - await store.addKey(key1); - await store.addKey(key2); - await store.addKey(key3); + await store.removeKeys(['1', '2']); - await expect(store.listKeys()).resolves.toEqual({ - items: [ - { key: key1, createdAt: expect.anything() }, - { key: key2, createdAt: expect.anything() }, - { key: key3, createdAt: expect.anything() }, - ], - }); + await expect(store.listKeys()).resolves.toEqual({ + items: [{ key: key3, createdAt: expect.anything() }], + }); - store.removeKeys(['1', '2', '3']); + await store.removeKeys([]); - await expect(store.listKeys()).resolves.toEqual({ - items: [], - }); - }); + await expect(store.listKeys()).resolves.toEqual({ + items: [{ key: key3, createdAt: expect.anything() }], + }); + + await store.removeKeys(['3', '4']); + + await expect(store.listKeys()).resolves.toEqual({ + items: [], + }); + + await store.addKey(key1); + await store.addKey(key2); + await store.addKey(key3); + + await expect(store.listKeys()).resolves.toEqual({ + items: [ + { key: key1, createdAt: expect.anything() }, + { key: key2, createdAt: expect.anything() }, + { key: key3, createdAt: expect.anything() }, + ], + }); + + await store.removeKeys(['1', '2', '3']); + + await expect(store.listKeys()).resolves.toEqual({ + items: [], + }); + }, + ); }); diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index 8039837469..0559b698e9 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -14,19 +14,10 @@ * limitations under the License. */ -import { - PluginDatabaseManager, - resolvePackagePath, -} from '@backstage/backend-common'; import { Knex } from 'knex'; import { DateTime } from 'luxon'; import { AnyJWK, KeyStore, StoredKey } from './types'; -const migrationsDir = resolvePackagePath( - '@backstage/plugin-auth-backend', - 'migrations', -); - const TABLE = 'signing_keys'; type Row = { @@ -35,10 +26,6 @@ type Row = { key: string; }; -type Options = { - database: PluginDatabaseManager; -}; - const parseDate = (date: string | Date) => { const parsedDate = typeof date === 'string' @@ -55,24 +42,7 @@ const parseDate = (date: string | Date) => { }; export class DatabaseKeyStore implements KeyStore { - static async create(options: Options): Promise { - const { database } = options; - const client = await database.getClient(); - - if (!database.migrations?.skip) { - await client.migrate.latest({ - directory: migrationsDir, - }); - } - - return new DatabaseKeyStore(client); - } - - private readonly client: Knex; - - private constructor(client: Knex) { - this.client = client; - } + constructor(private readonly client: Knex) {} async addKey(key: AnyJWK): Promise { await this.client(TABLE).insert({ diff --git a/plugins/auth-backend/src/identity/KeyStores.test.ts b/plugins/auth-backend/src/identity/KeyStores.test.ts index 375db4698b..6c034eb64b 100644 --- a/plugins/auth-backend/src/identity/KeyStores.test.ts +++ b/plugins/auth-backend/src/identity/KeyStores.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { DatabaseManager } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; - -import { MemoryKeyStore } from './MemoryKeyStore'; +import { AuthDatabase } from '../database/AuthDatabase'; import { DatabaseKeyStore } from './DatabaseKeyStore'; import { FirestoreKeyStore } from './FirestoreKeyStore'; import { KeyStores } from './KeyStores'; +import { MemoryKeyStore } from './MemoryKeyStore'; +import { getVoidLogger } from '@backstage/backend-common'; describe('KeyStores', () => { const defaultConfigOptions = { @@ -34,7 +34,10 @@ describe('KeyStores', () => { it('reads auth section from config', async () => { const configSpy = jest.spyOn(defaultConfig, 'getOptionalConfig'); - const keyStore = await KeyStores.fromConfig(defaultConfig); + const keyStore = await KeyStores.fromConfig(defaultConfig, { + logger: getVoidLogger(), + database: AuthDatabase.forTesting(), + }); expect(keyStore).toBeInstanceOf(MemoryKeyStore); expect(configSpy).toHaveBeenCalledWith('auth.keyStore'); @@ -46,18 +49,10 @@ describe('KeyStores', () => { }); it('can handle without auth config', async () => { - const config = new ConfigReader({ - backend: { - database: { - client: 'better-sqlite3', - connection: ':memory:', - }, - }, + const keyStore = await KeyStores.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), + database: AuthDatabase.forTesting(), }); - const database = - DatabaseManager.fromConfig(config).forPlugin('auth-backend'); - const keyStore = await KeyStores.fromConfig(config, { database }); - expect(keyStore).toBeInstanceOf(DatabaseKeyStore); }); @@ -82,7 +77,10 @@ describe('KeyStores', () => { }, }; const config = new ConfigReader(configOptions); - const keyStore = await KeyStores.fromConfig(config); + const keyStore = await KeyStores.fromConfig(config, { + logger: getVoidLogger(), + database: AuthDatabase.forTesting(), + }); expect(keyStore).toBeInstanceOf(FirestoreKeyStore); expect(createSpy).toHaveBeenCalledWith( diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts index e29b4b6fc4..4cd8f91842 100644 --- a/plugins/auth-backend/src/identity/KeyStores.ts +++ b/plugins/auth-backend/src/identity/KeyStores.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -import { Logger } from 'winston'; import { pickBy } from 'lodash'; +import { Logger } from 'winston'; -import { PluginDatabaseManager } from '@backstage/backend-common'; import { Config } from '@backstage/config'; +import { AuthDatabase } from '../database/AuthDatabase'; import { DatabaseKeyStore } from './DatabaseKeyStore'; -import { MemoryKeyStore } from './MemoryKeyStore'; import { FirestoreKeyStore } from './FirestoreKeyStore'; +import { MemoryKeyStore } from './MemoryKeyStore'; import { KeyStore } from './types'; type Options = { - logger?: Logger; - database?: PluginDatabaseManager; + logger: Logger; + database: AuthDatabase; }; export class KeyStores { @@ -37,23 +37,16 @@ export class KeyStores { * * @returns a KeyStore store */ - static async fromConfig( - config: Config, - options?: Options, - ): Promise { - const { logger, database } = options ?? {}; + static async fromConfig(config: Config, options: Options): Promise { + const { logger, database } = options; const ks = config.getOptionalConfig('auth.keyStore'); const provider = ks?.getOptionalString('provider') ?? 'database'; - logger?.info(`Configuring "${provider}" as KeyStore provider`); + logger.info(`Configuring "${provider}" as KeyStore provider`); if (provider === 'database') { - if (!database) { - throw new Error('This KeyStore provider requires a database'); - } - - return await DatabaseKeyStore.create({ database }); + return new DatabaseKeyStore(await database.get()); } if (provider === 'memory') { diff --git a/plugins/auth-backend/src/migrations.test.ts b/plugins/auth-backend/src/migrations.test.ts new file mode 100644 index 0000000000..194bbbf44c --- /dev/null +++ b/plugins/auth-backend/src/migrations.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Knex } from 'knex'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import fs from 'fs'; + +const migrationsDir = `${__dirname}/../migrations`; +const migrationsFiles = fs.readdirSync(migrationsDir).sort(); + +async function migrateUpOnce(knex: Knex): Promise { + await knex.migrate.up({ directory: migrationsDir }); +} + +async function migrateDownOnce(knex: Knex): Promise { + await knex.migrate.down({ directory: migrationsDir }); +} + +async function migrateUntilBefore(knex: Knex, target: string): Promise { + const index = migrationsFiles.indexOf(target); + if (index === -1) { + throw new Error(`Migration ${target} not found`); + } + for (let i = 0; i < index; i++) { + await migrateUpOnce(knex); + } +} + +jest.setTimeout(60_000); + +describe('migrations', () => { + const databases = TestDatabases.create({ + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + it.each(databases.eachSupportedId())( + '20230428155633_sessions.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20230428155633_sessions.js'); + await migrateUpOnce(knex); + + // Ensure that large cookies are supported + const data = `{"cookie":"${'a'.repeat(100_000)}"}`; + await knex + .insert({ sid: 'abc', expired: knex.fn.now(), sess: data }) + .into('sessions'); + await knex + .insert({ sid: 'def', expired: knex.fn.now(), sess: data }) + .into('sessions'); + + await expect(knex('sessions').orderBy('sid', 'asc')).resolves.toEqual([ + { sid: 'abc', expired: expect.anything(), sess: data }, + { sid: 'def', expired: expect.anything(), sess: data }, + ]); + + await migrateDownOnce(knex); + + await knex.destroy(); + }, + ); +}); diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts index 0d8f736550..6313276508 100644 --- a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts +++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts @@ -227,7 +227,7 @@ export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers { { headers }, ); if (!res.ok) { - throw ResponseError.fromResponse(res); + throw await ResponseError.fromResponse(res); } const cfIdentity = await res.json(); return cfIdentity as unknown as CloudflareAccessIdentityProfile; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 8013777415..2996ff68be 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -32,9 +32,11 @@ import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; +import connectSessionKnex from 'connect-session-knex'; import passport from 'passport'; import { Minimatch } from 'minimatch'; import { CatalogAuthResolverContext } from '../lib/resolvers'; +import { AuthDatabase } from '../database/AuthDatabase'; /** @public */ export type ProviderFactories = { [s: string]: AuthProviderFactory }; @@ -70,7 +72,11 @@ export async function createRouter( const appUrl = config.getString('app.baseUrl'); const authUrl = await discovery.getExternalBaseUrl('auth'); - const keyStore = await KeyStores.fromConfig(config, { logger, database }); + const authDb = AuthDatabase.create(database); + const keyStore = await KeyStores.fromConfig(config, { + logger, + database: authDb, + }); const keyDurationSeconds = 3600; const tokenIssuer = new TokenFactory({ @@ -84,14 +90,18 @@ export async function createRouter( const secret = config.getOptionalString('auth.session.secret'); if (secret) { router.use(cookieParser(secret)); - // TODO: Configure the server-side session storage. The default MemoryStore is not designed for production const enforceCookieSSL = authUrl.startsWith('https'); + const KnexSessionStore = connectSessionKnex(session); router.use( session({ secret, saveUninitialized: false, resave: false, cookie: { secure: enforceCookieSSL ? 'auto' : false }, + store: new KnexSessionStore({ + createtable: false, + knex: await authDb.get(), + }), }), ); router.use(passport.initialize()); diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index a0328bcb22..608f88437c 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-azure-devops +## 0.3.0-next.2 + +### Minor Changes + +- 877df261085: The getBuildRuns function now checks contains multiple comma-separated builds and splits them to send multiple requests for each and concatenates the results. + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index 5079fa4f16..7eeae09813 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -70,7 +70,7 @@ dev.azure.com/project: dev.azure.com/build-definition: ``` -In this case `` will be the name of your Team Project and `` will be the name of the Build Definition you would like to see Builds for. If the Build Definition name has spaces in it make sure to put quotes around it +In this case `` will be the name of your Team Project and `` will be the name of the Build Definition you would like to see Builds for, and it's possible to add more Builds separated by a comma. If the Build Definition name has spaces in it make sure to put quotes around it. ### Azure Pipelines Component diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 914cc9a4ba..8b3f4a8496 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.2.9-next.1", + "version": "0.3.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index 68d8896538..a202008798 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -120,7 +120,23 @@ export class AzureDevOpsClient implements AzureDevOpsApi { queryString.append('repoName', repoName); } if (definitionName) { - queryString.append('definitionName', definitionName); + const definitionNames = definitionName.split(','); + if (definitionNames.length > 1) { + const buildRuns: BuildRun[] = []; + for (const name of definitionNames) { + queryString.set('definitionName', name.trim()); + if (options?.top) { + queryString.set('top', options.top.toString()); + } + const urlSegment = `builds/${encodeURIComponent( + projectName, + )}?${queryString}`; + const items = await this.get(urlSegment); + buildRuns.push(...items); + } + return { items: buildRuns }; + } + queryString.append('definitionName', definitionName.trim()); } if (options?.top) { queryString.append('top', options.top.toString()); diff --git a/plugins/azure-sites-backend/README.md b/plugins/azure-sites-backend/README.md index 0d44b9a4e4..23152f9760 100644 --- a/plugins/azure-sites-backend/README.md +++ b/plugins/azure-sites-backend/README.md @@ -2,7 +2,7 @@ Simple plugin that proxies requests to the Azure Portal API through Azure SDK JavaScript libraries. -_Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_ +_Inspired by [roadie.io AWS Lambda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_ ## Setup @@ -65,7 +65,7 @@ Here's how to get the backend plugin up and running: ```ts import azure from './plugins/azure'; - // Removed for clairty... + // Removed for clarity... async function main() { // ... @@ -82,4 +82,4 @@ Here's how to get the backend plugin up and running: 4. Now run `yarn start-backend` from the repo root. -5. Finally, open `http://localhost:7007/api/azure/health` in a browser, it should return `{"status":"ok"}`. +5. Finally, open `http://localhost:7007/api/azure-sites/health` in a browser, it should return `{"status":"ok"}`. diff --git a/plugins/azure-sites/CHANGELOG.md b/plugins/azure-sites/CHANGELOG.md index 1eea015834..20703e33a5 100644 --- a/plugins/azure-sites/CHANGELOG.md +++ b/plugins/azure-sites/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-azure-sites +## 0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.7-next.1 ### Patch Changes diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json index 8863e76dae..c3335d6129 100644 --- a/plugins/azure-sites/package.json +++ b/plugins/azure-sites/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites", - "version": "0.1.7-next.1", + "version": "0.1.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index 50bf55c918..c543efb0a6 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-badges-backend +## 0.2.0-next.2 + +### Minor Changes + +- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges. + + Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version. + + **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.7 + ## 0.1.39-next.1 ### Patch Changes diff --git a/plugins/badges-backend/README.md b/plugins/badges-backend/README.md index 63e43c6692..ddbb30bc4e 100644 --- a/plugins/badges-backend/README.md +++ b/plugins/badges-backend/README.md @@ -36,6 +36,9 @@ export default async function createPlugin( config: env.config, discovery: env.discovery, badgeFactories: createDefaultBadgeFactories(), + tokenManager: env.tokenManager, + logger: env.logger, + identity: env.identity, }); } ``` @@ -112,11 +115,29 @@ export const createMyCustomBadgeFactories = (): BadgeFactories => ({ }); ``` +### Badge obfuscation + +When you enable the obfuscation feature, the badges backend will obfuscate the entity names in the badge link. It's useful when you want your badges to be visible to the public, but you don't want to expose the entity names and also to protect your entity names from being enumerated. + +To enable the obfuscation you need to activate the `obfuscation` feature in the `app-config.yaml`: + +```yaml +app: + badges: + obfuscate: true +``` + +:warning: **Warning**: The only endpoint to be publicly available is the `/entity/:entityUuid/:badgeId` endpoint. The other endpoints are meant for trusted internal users and should not be publicly exposed. + +> Note that you cannot use env vars to set the `obfuscate` value. It must be a boolean value and env vars are always strings. + ## API The badges backend api exposes two main endpoints for entity badges. The `/badges` prefix is arbitrary, and the default for the example backend. +### If obfuscation is disabled (default or apps.badges.obfuscate: false) + - `/badges/entity/:namespace/:kind/:name/badge-specs` List all defined badges for a particular entity, in json format. See [BadgeSpec](https://github.com/backstage/backstage/tree/master/plugins/badges/src/api/types.ts) @@ -126,6 +147,21 @@ The badges backend api exposes two main endpoints for entity badges. The an SVG image. If the `accept` request header prefers `application/json` the badge spec as JSON will be returned instead of the image. +### If obfuscation is enabled (apps.badges.obfuscate: true) + +- `/badges/entity/:namespace/:kind/:name/obfuscated` Get the obfuscated `entity url`. + +> Note that endpoint have a embedded authMiddleware to authenticate the user requesting this endpoint. _It meant to be called from the frontend plugin._ + +- `/badges/entity/:entityUuid/:badgeId` Get the entity badge as an SVG image. If + the `accept` request header prefers `application/json` the badge spec as JSON + will be returned instead of the image. + +- `/badge/entity/:entityUuid/badge-specs` List all defined badges for a + particular entity, in json format. See + [BadgeSpec](https://github.com/backstage/backstage/tree/master/plugins/badges/src/api/types.ts) + from the frontend plugin for a type declaration. + ## Links - [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/badges) diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md index 8ca4b561a2..09a02ba44d 100644 --- a/plugins/badges-backend/api-report.md +++ b/plugins/badges-backend/api-report.md @@ -7,7 +7,10 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { TokenManager } from '@backstage/backend-common'; // @public (undocumented) export interface Badge { @@ -78,6 +81,27 @@ export type BadgeSpec = { markdown: string; }; +// @public +export interface BadgesStore { + // (undocumented) + getBadgeFromUuid(uuid: string): Promise< + | { + name: string; + namespace: string; + kind: string; + } + | undefined + >; + // (undocumented) + getBadgeUuid( + name: string, + namespace: string, + kind: string, + ): Promise<{ + uuid: string; + }>; +} + // @public (undocumented) export type BadgeStyle = (typeof BADGE_STYLES)[number]; @@ -107,10 +131,18 @@ export interface RouterOptions { // (undocumented) badgeFactories?: BadgeFactories; // (undocumented) + badgeStore?: BadgesStore; + // (undocumented) catalog?: CatalogApi; // (undocumented) config: Config; // (undocumented) discovery: PluginEndpointDiscovery; + // (undocumented) + identity: IdentityApi; + // (undocumented) + logger: Logger; + // (undocumented) + tokenManager: TokenManager; } ``` diff --git a/plugins/badges-backend/migrations/20230404_init.js b/plugins/badges-backend/migrations/20230404_init.js new file mode 100644 index 0000000000..3de6bfc248 --- /dev/null +++ b/plugins/badges-backend/migrations/20230404_init.js @@ -0,0 +1,33 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + await knex.schema.createTable('badges', table => { + table.string('kind').notNullable(); + table.string('namespace').notNullable(); + table.string('name').notNullable(); + table.string('uuid').unique().notNullable(); + table.index(['uuid'], 'badges_uuid_index'); + table.primary(['kind', 'namespace', 'name']); + }); +}; + +exports.down = async function down(knex) { + await knex.schema.alterTable('badges', table => { + table.dropIndex('', 'badges_uuid_index'); + }); + await knex.schema.dropTable('badges'); +}; diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 5f9b5b51a3..84b4d324d7 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.1.39-next.1", + "version": "0.2.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,18 +38,25 @@ "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", "@types/express": "^4.17.6", "badge-maker": "^3.3.0", "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", + "knex": "^2.4.2", + "lodash": "^4.17.21", + "supertest": "^6.3.3", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/catalog-client": "workspace:^", "@backstage/cli": "workspace:^", - "@types/supertest": "^2.0.8", - "supertest": "^6.1.3" + "@types/node": "*", + "cross-fetch": "^3.1.5" }, "files": [ "dist" diff --git a/plugins/badges-backend/src/database/badgeStore.test.ts b/plugins/badges-backend/src/database/badgeStore.test.ts new file mode 100644 index 0000000000..5a7a82d4b7 --- /dev/null +++ b/plugins/badges-backend/src/database/badgeStore.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DatabaseBadgesStore } from './badgesStore'; +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { Knex } from 'knex'; +import { Entity } from '@backstage/catalog-model'; + +describe('DatabaseBadgesStore', () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test', + }, + }; + + const databases = TestDatabases.create(); + + async function createDatabaseBadgesStore(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + return { + knex, + badgeStore: await DatabaseBadgesStore.create({ + database: { getClient: async () => knex }, + }), + }; + } + + describe.each(databases.eachSupportedId())('%p', databaseId => { + let knex: Knex; + let badgeStore: DatabaseBadgesStore; + + beforeEach(async () => { + ({ knex, badgeStore } = await createDatabaseBadgesStore(databaseId)); + }); + + it('createABadge if not existing in DB', async () => { + const uuid = await badgeStore.getBadgeUuid( + entity.metadata.name, + entity.metadata.namespace || 'default', + entity.kind, + ); + + expect(uuid.uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); + + const storedBadge = await badgeStore.getBadgeFromUuid(uuid.uuid); + expect(storedBadge?.kind).toEqual(entity.kind); + expect(storedBadge?.name).toEqual(entity.metadata.name); + expect(storedBadge?.namespace).toEqual( + entity.metadata.namespace || 'default', + ); + }); + + it('getBadge if badge already exist in DB', async () => { + await knex('badges').truncate(); + await knex('badges').insert([ + { + uuid: 'uuid1', + name: 'test', + namespace: 'default', + kind: 'component', + }, + ]); + + const storedEntity = await badgeStore.getBadgeFromUuid('uuid1'); + + expect(storedEntity).toEqual({ + name: 'test', + namespace: 'default', + kind: 'component', + }); + }); + + it('getBadgeUuid if badge exist in DB', async () => { + await knex('badges').truncate(); + await knex('badges').insert([ + { + uuid: 'uuid1', + name: 'test', + namespace: 'default', + kind: 'component', + }, + ]); + + const storedUuid = await badgeStore.getBadgeUuid( + 'test', + 'default', + 'component', + ); + + expect(storedUuid).toEqual({ uuid: 'uuid1' }); + }); + }); +}); diff --git a/plugins/badges-backend/src/database/badgesStore.ts b/plugins/badges-backend/src/database/badgesStore.ts new file mode 100644 index 0000000000..e6c6a9e2ad --- /dev/null +++ b/plugins/badges-backend/src/database/badgesStore.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + PluginDatabaseManager, + resolvePackagePath, +} from '@backstage/backend-common'; +import { Knex } from 'knex'; +import { isNil } from 'lodash'; +import { v4 as uuidv4 } from 'uuid'; + +/** + * internal + * @public + */ +export interface BadgesStore { + getBadgeUuid( + name: string, + namespace: string, + kind: string, + ): Promise<{ uuid: string }>; + + getBadgeFromUuid( + uuid: string, + ): Promise<{ name: string; namespace: string; kind: string } | undefined>; +} + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-badges-backend', // Package name + 'migrations', // Migrations directory +); + +/** + * DatabaseBadgesStore + * @internal + */ +export class DatabaseBadgesStore implements BadgesStore { + private constructor(private readonly db: Knex) {} + + static async create({ + database, + skipMigrations, + }: { + database: PluginDatabaseManager; + skipMigrations?: boolean; + }): Promise { + const client = await database.getClient(); + + if (!database.migrations?.skip && !skipMigrations) { + await client.migrate.latest({ + directory: migrationsDir, + }); + } + + return new DatabaseBadgesStore(client); + } + + async getBadgeFromUuid( + uuid: string, + ): Promise<{ name: string; namespace: string; kind: string } | undefined> { + const result = await this.db('badges') + .select('namespace', 'name', 'kind') + .where({ uuid: uuid }) + .first(); + + return result; + } + + async getBadgeUuid( + name: string, + namespace: string, + kind: string, + ): Promise<{ uuid: string }> { + const result = await this.db('badges') + .select('uuid') + .where({ name: name, namespace: namespace, kind: kind }) + .first(); + + let uuid = result?.uuid; + + if (isNil(uuid)) { + uuid = uuidv4(); + + await this.db('badges') + .insert({ + uuid: uuid, + name: name, + namespace: namespace, + kind: kind, + }) + .onConflict(['name', 'namespace', 'kind']) + .ignore(); + } + + return { uuid }; + } +} diff --git a/plugins/badges-backend/src/index.ts b/plugins/badges-backend/src/index.ts index a45714cb81..35a061c516 100644 --- a/plugins/badges-backend/src/index.ts +++ b/plugins/badges-backend/src/index.ts @@ -24,3 +24,4 @@ export * from './badges'; export * from './lib'; export * from './service/router'; export * from './types'; +export * from './database/badgesStore'; diff --git a/plugins/badges-backend/src/service/router-obfuscated.test.ts b/plugins/badges-backend/src/service/router-obfuscated.test.ts new file mode 100644 index 0000000000..3c9ec0d2b6 --- /dev/null +++ b/plugins/badges-backend/src/service/router-obfuscated.test.ts @@ -0,0 +1,302 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import request from 'supertest'; +import { + getVoidLogger, + PluginEndpointDiscovery, + ServerTokenManager, + SingleHostDiscovery, +} from '@backstage/backend-common'; +import { CatalogApi } from '@backstage/catalog-client'; +import type { Entity } from '@backstage/catalog-model'; +import { Config, ConfigReader } from '@backstage/config'; +import { createRouter } from './router'; +import { BadgeBuilder } from '../lib'; +import { + BackstageIdentityResponse, + IdentityApiGetIdentityRequest, +} from '@backstage/plugin-auth-node'; +import { BadgesStore } from '../database/badgesStore'; + +describe('createRouter', () => { + let app: express.Express; + const badgeBuilder: jest.Mocked = { + getBadges: jest.fn(), + createBadgeJson: jest.fn(), + createBadgeSvg: jest.fn(), + }; + + const catalog = { + addLocation: jest.fn(), + getEntities: jest.fn(), + getEntityByRef: jest.fn(), + getLocationByRef: jest.fn(), + getLocationById: jest.fn(), + removeLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), + validateEntity: jest.fn(), + }; + const getIdentity = jest + .fn() + .mockImplementation( + async ({ + request: _request, + }: IdentityApiGetIdentityRequest): Promise< + BackstageIdentityResponse | undefined + > => { + return { + identity: { + userEntityRef: 'user:default/guest', + ownershipEntityRefs: [], + type: 'user', + }, + token: 'token', + }; + }, + ); + + const config: Config = new ConfigReader({ + backend: { + baseUrl: 'http://127.0.0.1', + listen: { + port: 7007, + }, + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + app: { + badges: { + obfuscate: true, + }, + }, + }); + + let discovery: PluginEndpointDiscovery; + + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test', + }, + }; + + const entities: Entity[] = [ + entity, + { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test-2', + }, + }, + ]; + + const badge = { + id: 'test-badge', + badge: { + label: 'test', + message: 'badge', + }, + url: '/...', + markdown: '[![...](...)]', + }; + + const badgeEntity = { + name: 'test', + namespace: 'default', + kind: 'component', + }; + + const badgeStore: jest.Mocked = { + getBadgeUuid: jest.fn().mockImplementation(async () => { + return { uuid: 'uuid1' }; + }), + getBadgeFromUuid: jest.fn().mockImplementation(async () => { + return badgeEntity; + }), + }; + + beforeAll(async () => { + discovery = SingleHostDiscovery.fromConfig(config); + const tokenManager = ServerTokenManager.noop(); + const router = await createRouter({ + badgeBuilder, + catalog: catalog as Partial as CatalogApi, + config, + discovery, + tokenManager, + logger: getVoidLogger(), + identity: { getIdentity }, + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('works with provided badgeStore', async () => { + const tokenManager = ServerTokenManager.noop(); + const router = await createRouter({ + badgeBuilder, + catalog: catalog as Partial as CatalogApi, + config, + discovery, + tokenManager, + logger: getVoidLogger(), + identity: { getIdentity }, + badgeStore: badgeStore, + }); + expect(router).toBeDefined(); + }); + + describe('GET /entity/:namespace/:kind/:name/badge-specs', () => { + it('does not returns all badge specs for entity', async () => { + catalog.getEntityByRef.mockResolvedValueOnce(entity); + + badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]); + badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); + + const response = await request(app).get( + '/entity/default/component/test/badge-specs', + ); + + expect(response.status).toEqual(404); + }); + }); + + describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => { + it('does not returns badge for entity', async () => { + catalog.getEntityByRef.mockResolvedValueOnce(entity); + + const image = '...'; + badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image); + + const response = await request(app).get( + '/entity/default/component/test/badge/test-badge', + ); + + expect(response.status).toEqual(404); + }); + + it('does not returns badge spec for entity', async () => { + catalog.getEntityByRef.mockResolvedValueOnce(entity); + badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); + + const url = '/entity/default/component/test/badge/test-badge?format=json'; + const response = await request(app).get(url); + + expect(response.status).toEqual(404); + }); + }); + + describe('GET /entity/:namespace/:kind/:name/obfuscated', () => { + catalog.getEntityByRef.mockResolvedValueOnce(entity); + catalog.getEntities.mockResolvedValueOnce({ items: entities }); + + it('returns obfuscated 401 if no auth', async () => { + const obfuscatedEntity = await request(app).get( + '/entity/default/component/test/obfuscated', + ); + expect(obfuscatedEntity.status).toEqual(401); + }); + + it('returns obfuscated entity and badges', async () => { + const obfuscatedEntity = await request(app) + .get('/entity/default/component/test/obfuscated') + .set('Authorization', 'Bearer fakeToken'); + expect(obfuscatedEntity.status).toEqual(200); + expect(obfuscatedEntity.body.uuid).toMatch( + new RegExp( + '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', + ), + ); + + const uuid = obfuscatedEntity.body.uuid; + const url = `/entity/${uuid}/test-badge?format=json`; + let response = await request(app).get(url); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(badge); + + catalog.getEntityByRef.mockResolvedValueOnce(entity); + catalog.getEntities.mockResolvedValueOnce({ items: entities }); + + const image = '...'; + badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image); + + response = await request(app).get(`/entity/${uuid}/test-badge`); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(Buffer.from(image)); + + catalog.getEntities.mockResolvedValueOnce({ items: entities }); + catalog.getEntityByRef.mockResolvedValueOnce(entity); + + badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]); + badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); + + response = await request(app).get(`/entity/${uuid}/badge-specs`); + expect(response.status).toEqual(200); + expect(response.body).toEqual([badge]); + + expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1); + expect(badgeBuilder.createBadgeJson).toHaveBeenCalledTimes(2); + }); + + describe('Errors', () => { + it('returns 404 for unknown entity uuid', async () => { + badgeStore.getBadgeFromUuid.mockResolvedValue(undefined); + catalog.getEntityByRef.mockResolvedValueOnce(entity); + catalog.getEntities.mockResolvedValueOnce({ items: entities }); + badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]); + badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); + + async function testUrl(url: string) { + const response = await request(app).get(url); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ + error: { + message: expect.any(String), + name: 'NotFoundError', + }, + request: { + method: 'GET', + url, + }, + response: { + statusCode: 404, + }, + }); + } + await testUrl( + '/entity/3a5f91c1e66519be5394c37a8ba69cfsf3087b7c322c600e7497dc9d517353e5bed/badge-specs', + ); + await testUrl( + '/entity/3a5f91c1e66519be5394c37a8ba69c3087b7csfsf322c600e7497dc9d517353e5bed/test-badge', + ); + }); + }); + }); +}); diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index 109b64231d..2ecae6b452 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -17,7 +17,9 @@ import express from 'express'; import request from 'supertest'; import { + getVoidLogger, PluginEndpointDiscovery, + ServerTokenManager, SingleHostDiscovery, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; @@ -25,10 +27,16 @@ import type { Entity } from '@backstage/catalog-model'; import { Config, ConfigReader } from '@backstage/config'; import { createRouter } from './router'; import { BadgeBuilder } from '../lib'; +import { + BackstageIdentityResponse, + IdentityApiGetIdentityRequest, +} from '@backstage/plugin-auth-node'; +import { BadgesStore } from '../database/badgesStore'; describe('createRouter', () => { let app: express.Express; let badgeBuilder: jest.Mocked; + const catalog = { addLocation: jest.fn(), getEntities: jest.fn(), @@ -42,12 +50,15 @@ describe('createRouter', () => { getEntityFacets: jest.fn(), validateEntity: jest.fn(), }; + + const getIdentity = jest.fn(); + let config: Config; let discovery: PluginEndpointDiscovery; const entity: Entity = { apiVersion: 'v1', - kind: 'service', + kind: 'component', metadata: { name: 'test', }, @@ -63,6 +74,11 @@ describe('createRouter', () => { markdown: '[![...](...)]', }; + const badgeStore: jest.Mocked = { + getBadgeFromUuid: jest.fn(), + getBadgeUuid: jest.fn(), + }; + beforeAll(async () => { badgeBuilder = { getBadges: jest.fn(), @@ -73,15 +89,40 @@ describe('createRouter', () => { backend: { baseUrl: 'http://127.0.0.1', listen: { port: 7007 }, + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, }, }); - discovery = SingleHostDiscovery.fromConfig(config); + getIdentity.mockImplementation( + async ({ + request: _request, + }: IdentityApiGetIdentityRequest): Promise< + BackstageIdentityResponse | undefined + > => { + return { + identity: { + userEntityRef: 'user:default/guest', + ownershipEntityRefs: [], + type: 'user', + }, + token: 'token', + }; + }, + ); + + discovery = SingleHostDiscovery.fromConfig(config); + const tokenManager = ServerTokenManager.noop(); const router = await createRouter({ badgeBuilder, catalog: catalog as Partial as CatalogApi, config, discovery, + tokenManager, + logger: getVoidLogger(), + identity: { getIdentity }, }); app = express().use(router); }); @@ -90,12 +131,17 @@ describe('createRouter', () => { jest.resetAllMocks(); }); - it('works', async () => { + it('works with badgeStore', async () => { + const tokenManager = ServerTokenManager.noop(); const router = await createRouter({ badgeBuilder, catalog: catalog as Partial as CatalogApi, config, discovery, + tokenManager, + logger: getVoidLogger(), + identity: { getIdentity }, + badgeStore: badgeStore, }); expect(router).toBeDefined(); }); @@ -108,7 +154,7 @@ describe('createRouter', () => { badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); const response = await request(app).get( - '/entity/default/service/test/badge-specs', + '/entity/default/component/test/badge-specs', ); expect(response.status).toEqual(200); @@ -118,10 +164,10 @@ describe('createRouter', () => { expect(catalog.getEntityByRef).toHaveBeenCalledWith( { namespace: 'default', - kind: 'service', + kind: 'component', name: 'test', }, - { token: undefined }, + { token: '' }, ); expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1); @@ -130,46 +176,7 @@ describe('createRouter', () => { badgeInfo: { id: badge.id }, context: { badgeUrl: expect.stringMatching( - /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/service\/test\/badge\/test-badge/, - ), - config, - entity, - }, - }); - }); - }); - - describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => { - it('returns badge for entity', async () => { - catalog.getEntityByRef.mockResolvedValueOnce(entity); - - const image = '...'; - badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image); - - const response = await request(app).get( - '/entity/default/service/test/badge/test-badge', - ); - - expect(response.status).toEqual(200); - expect(response.body).toEqual(Buffer.from(image)); - - expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); - expect(catalog.getEntityByRef).toHaveBeenCalledWith( - { - namespace: 'default', - kind: 'service', - name: 'test', - }, - { token: undefined }, - ); - - expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(0); - expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledTimes(1); - expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledWith({ - badgeInfo: { id: badge.id }, - context: { - badgeUrl: expect.stringMatching( - /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/service\/test\/badge\/test-badge/, + /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/component\/test\/badge\/test-badge/, ), config, entity, @@ -177,27 +184,90 @@ describe('createRouter', () => { }); }); - it('returns badge spec for entity', async () => { - catalog.getEntityByRef.mockResolvedValueOnce(entity); - badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); + describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => { + it('returns badge for entity', async () => { + catalog.getEntityByRef.mockResolvedValueOnce(entity); - const url = '/entity/default/service/test/badge/test-badge?format=json'; - const response = await request(app).get(url); + const image = '...'; + badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image); - expect(response.status).toEqual(200); - expect(response.body).toEqual(badge); + const response = await request(app).get( + '/entity/default/component/test/badge/test-badge', + ); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(Buffer.from(image)); + + expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); + expect(catalog.getEntityByRef).toHaveBeenCalledWith( + { + namespace: 'default', + kind: 'component', + name: 'test', + }, + { token: '' }, + ); + + expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(0); + expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledTimes(1); + expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledWith({ + badgeInfo: { id: badge.id }, + context: { + badgeUrl: expect.stringMatching( + /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/component\/test\/badge\/test-badge/, + ), + config, + entity, + }, + }); + }); + + it('returns badge spec for entity', async () => { + catalog.getEntityByRef.mockResolvedValueOnce(entity); + badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); + + const url = + '/entity/default/component/test/badge/test-badge?format=json'; + const response = await request(app).get(url); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(badge); + }); }); - }); - describe('Errors', () => { - it('returns 404 for unknown entities', async () => { + describe('Errors', () => { + it('returns 404 for unknown entities', async () => { + catalog.getEntityByRef.mockResolvedValue(undefined); + async function testUrl(url: string) { + const response = await request(app).get(url); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ + error: { + message: 'No component entity in default named "missing"', + name: 'NotFoundError', + }, + request: { + method: 'GET', + url, + }, + response: { + statusCode: 404, + }, + }); + } + await testUrl('/entity/default/component/missing/badge-specs'); + await testUrl('/entity/default/component/missing/badge/test-badge'); + }); + }); + + it('returns 404 for uuid entities', async () => { catalog.getEntityByRef.mockResolvedValue(undefined); async function testUrl(url: string) { const response = await request(app).get(url); expect(response.status).toEqual(404); expect(response.body).toEqual({ error: { - message: 'No service entity in default named "missing"', + message: 'No component entity in default named "missing"', name: 'NotFoundError', }, request: { @@ -209,8 +279,8 @@ describe('createRouter', () => { }, }); } - await testUrl('/entity/default/service/missing/badge-specs'); - await testUrl('/entity/default/service/missing/badge/test-badge'); + await testUrl('/entity/default/component/missing/badge-specs'); + await testUrl('/entity/default/component/missing/badge/test-badge'); }); }); }); diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts index e5caa5cd2a..308865b73a 100644 --- a/plugins/badges-backend/src/service/router.ts +++ b/plugins/badges-backend/src/service/router.ts @@ -17,14 +17,21 @@ import express from 'express'; import Router from 'express-promise-router'; import { + DatabaseManager, errorHandler, PluginEndpointDiscovery, + TokenManager, } from '@backstage/backend-common'; import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { NotFoundError } from '@backstage/errors'; +import { AuthenticationError, NotFoundError } from '@backstage/errors'; import { BadgeBuilder, DefaultBadgeBuilder } from '../lib/BadgeBuilder'; import { BadgeContext, BadgeFactories } from '../types'; +import { isNil } from 'lodash'; +import { Logger } from 'winston'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +import { BadgesStore, DatabaseBadgesStore } from '../database/badgesStore'; /** @public */ export interface RouterOptions { @@ -33,6 +40,10 @@ export interface RouterOptions { catalog?: CatalogApi; config: Config; discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + logger: Logger; + identity: IdentityApi; + badgeStore?: BadgesStore; } /** @public */ @@ -46,13 +57,214 @@ export async function createRouter( new DefaultBadgeBuilder(options.badgeFactories || {}); const router = Router(); + const { config, logger, tokenManager, discovery, identity } = options; + const baseUrl = await discovery.getExternalBaseUrl('badges'); + + if (config.getOptionalBoolean('app.badges.obfuscate')) { + return obfuscatedRoute( + router, + catalog, + badgeBuilder, + tokenManager, + logger, + options, + config, + identity, + baseUrl, + ); + } + return nonObfuscatedRoute( + router, + catalog, + badgeBuilder, + tokenManager, + config, + baseUrl, + ); +} + +async function obfuscatedRoute( + router: express.Router, + catalog: CatalogApi, + badgeBuilder: BadgeBuilder, + tokenManager: TokenManager, + logger: Logger, + options: RouterOptions, + config: Config, + identity: IdentityApi, + baseUrl: string, +) { + logger.info('Badges obfuscation is enabled'); + + const store = options.badgeStore + ? options.badgeStore + : await DatabaseBadgesStore.create({ + database: await DatabaseManager.fromConfig(config).forPlugin('badges'), + }); + + router.get('/entity/:entityUuid/badge-specs', async (req, res) => { + const { entityUuid } = req.params; + + // Retrieve the badge info from the database + const badgeInfos = await store.getBadgeFromUuid(entityUuid); + + if (isNil(badgeInfos)) { + throw new NotFoundError(`No badge found for entity uuid "${entityUuid}"`); + } + + // If a mapping is found, map name, namespace and kind + const name = badgeInfos.name; + const namespace = badgeInfos.namespace; + const kind = badgeInfos.kind; + const token = await tokenManager.getToken(); + + // Query the catalog with the name, namespace, kind to get the entity informations + const entity = await catalog.getEntityByRef( + { + namespace, + kind, + name, + }, + token, + ); + if (isNil(entity)) { + throw new NotFoundError( + `No ${kind} entity in ${namespace} named "${name}"`, + ); + } + + // Create the badge specs + const specs = []; + for (const badgeInfo of await badgeBuilder.getBadges()) { + const context: BadgeContext = { + badgeUrl: `${baseUrl}/entity/${entityUuid}/${badgeInfo.id}`, + config: config, + entity, + }; + + const badge = await badgeBuilder.createBadgeJson({ + badgeInfo, + context, + }); + specs.push(badge); + } + + res.status(200).json(specs); + }); + + router.get('/entity/:entityUuid/:badgeId', async (req, res) => { + const { entityUuid, badgeId } = req.params; + + // Retrieve the badge info from the database + const badgeInfo = await store.getBadgeFromUuid(entityUuid); + + if (isNil(badgeInfo)) { + throw new NotFoundError(`No badge found for entity uuid "${entityUuid}"`); + } + + // If a mapping is found, map name, namespace and kind + const name = badgeInfo.name; + const namespace = badgeInfo.namespace; + const kind = badgeInfo.kind; + const token = await tokenManager.getToken(); + const entity = await catalog.getEntityByRef( + { + namespace, + kind, + name, + }, + token, + ); + if (isNil(entity)) { + throw new NotFoundError( + `No ${kind} entity in ${namespace} named "${name}"`, + res.sendStatus(404), + ); + } + + let format = + req.accepts(['image/svg+xml', 'application/json']) || 'image/svg+xml'; + if (req.query.format === 'json') { + format = 'application/json'; + } + + const badgeOptions = { + badgeInfo: { id: badgeId }, + context: { + badgeUrl: `${baseUrl}/entity/${entityUuid}/${badgeId}`, + config: config, + entity, + }, + }; + + let data: string; + if (format === 'application/json') { + data = JSON.stringify( + await badgeBuilder.createBadgeJson(badgeOptions), + null, + 2, + ); + } else { + data = await badgeBuilder.createBadgeSvg(badgeOptions); + } + + res.setHeader('Content-Type', format); + res.status(200).send(data); + }); + + router.get( + '/entity/:namespace/:kind/:name/obfuscated', + function authenticate(req, _res, next) { + const token = + getBearerTokenFromAuthorizationHeader(req.headers.authorization) || + (req.cookies?.token as string | undefined); + + if (!token) { + throw new AuthenticationError('Unauthorized'); + } + + try { + req.user = identity.getIdentity({ request: req }); + next(); + } catch (error) { + tokenManager.authenticate(token.toString()); + next(error); + } + }, + async (req, res) => { + const { namespace, kind, name } = req.params; + const storedEntityUuid: { uuid: string } | undefined = + await store.getBadgeUuid(name, namespace, kind); + + if (isNil(storedEntityUuid)) { + throw new NotFoundError( + `No uuid found for entity "${namespace}/${kind}/${name}"`, + ); + } + + return res.status(200).json(storedEntityUuid); + }, + ); + + router.use(errorHandler()); + + return router; +} + +async function nonObfuscatedRoute( + router: express.Router, + catalog: CatalogApi, + badgeBuilder: BadgeBuilder, + tokenManager: TokenManager, + config: Config, + baseUrl: string, +) { router.get('/entity/:namespace/:kind/:name/badge-specs', async (req, res) => { + const token = await tokenManager.getToken(); const { namespace, kind, name } = req.params; const entity = await catalog.getEntityByRef( { namespace, kind, name }, - { - token: getBearerToken(req.headers.authorization), - }, + token, ); if (!entity) { throw new NotFoundError( @@ -62,19 +274,17 @@ export async function createRouter( const specs = []; for (const badgeInfo of await badgeBuilder.getBadges()) { + const badgeId = badgeInfo.id; const context: BadgeContext = { - badgeUrl: await getBadgeUrl( - namespace, - kind, - name, - badgeInfo.id, - options, - ), - config: options.config, + badgeUrl: `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`, + config: config, entity, }; - const badge = await badgeBuilder.createBadgeJson({ badgeInfo, context }); + const badge = await badgeBuilder.createBadgeJson({ + badgeInfo, + context, + }); specs.push(badge); } @@ -85,11 +295,10 @@ export async function createRouter( '/entity/:namespace/:kind/:name/badge/:badgeId', async (req, res) => { const { namespace, kind, name, badgeId } = req.params; + const token = await tokenManager.getToken(); const entity = await catalog.getEntityByRef( { namespace, kind, name }, - { - token: getBearerToken(req.headers.authorization), - }, + token, ); if (!entity) { throw new NotFoundError( @@ -106,8 +315,8 @@ export async function createRouter( const badgeOptions = { badgeInfo: { id: badgeId }, context: { - badgeUrl: await getBadgeUrl(namespace, kind, name, badgeId, options), - config: options.config, + badgeUrl: `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`, + config: config, entity, }, }; @@ -132,18 +341,3 @@ export async function createRouter( return router; } - -async function getBadgeUrl( - namespace: string, - kind: string, - name: string, - badgeId: string, - options: RouterOptions, -): Promise { - const baseUrl = await options.discovery.getExternalBaseUrl('badges'); - return `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`; -} - -function getBearerToken(header?: string): string | undefined { - return header?.match(/Bearer\s+(\S+)/i)?.[1]; -} diff --git a/plugins/badges-backend/src/service/standaloneServer.ts b/plugins/badges-backend/src/service/standaloneServer.ts index 0800585634..b07e1fa3b1 100644 --- a/plugins/badges-backend/src/service/standaloneServer.ts +++ b/plugins/badges-backend/src/service/standaloneServer.ts @@ -19,9 +19,14 @@ import { Logger } from 'winston'; import { createServiceBuilder, loadBackendConfig, + ServerTokenManager, SingleHostDiscovery, + useHotMemoize, } from '@backstage/backend-common'; import { createRouter } from './router'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import { DatabaseBadgesStore } from '../database/badgesStore'; +import Knex from 'knex'; export interface ServerOptions { port: number; @@ -35,10 +40,40 @@ export async function startStandaloneServer( const logger = options.logger.child({ service: 'badges-backend' }); const config = await loadBackendConfig({ logger, argv: process.argv }); const discovery = SingleHostDiscovery.fromConfig(config); + const database = useHotMemoize(module, () => { + return Knex({ + client: 'better-sqlite3', + connection: ':memory:', + useNullAsDefault: true, + }); + }); logger.debug('Creating application...'); - const router = await createRouter({ config, discovery }); + const tokenManager = ServerTokenManager.noop(); + const identity: IdentityApi = { + async getIdentity({ request }) { + const token = request.headers.authorization?.split(' ')[1]; + return { + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: token || 'user:default/john_doe', + }, + token: token || 'no-token', + }; + }, + }; + const router = await createRouter({ + config, + discovery, + tokenManager, + logger, + identity, + badgeStore: await DatabaseBadgesStore.create({ + database: { getClient: async () => database }, + }), + }); let service = createServiceBuilder(module) .setPort(options.port) diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index b4a0e6aec6..a05ef8405d 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-badges +## 0.2.42-next.2 + +### Patch Changes + +- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges. + + Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version. + + **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options. + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.42-next.1 ### Patch Changes diff --git a/plugins/badges/README.md b/plugins/badges/README.md index 65ace6226e..4b8ff38d47 100644 --- a/plugins/badges/README.md +++ b/plugins/badges/README.md @@ -18,6 +18,51 @@ This will popup a badges dialog showing all available badges for that entity lik ![Badges Dialog](./doc/badges-dialog.png) +##  Badge obfuscation + +The badges plugin supports obfuscating the badge URL to prevent it from being enumerated if the badges are used in a public context (like in Github repositories). + +To enable obfuscation, set the `obfuscate` option to `true` in the `app.badges` section of your `app-config.yaml`: + +```yaml +app: + badges: + obfuscate: true +``` + +Please note that if you have already set badges in your repositories and you activate the obfuscation you will need to update the badges in your repositories to use the new obfuscated URLs. + +Please note that the backend part needs to be configured to support obfuscation. See the [backend plugin documentation](../badges-backend/README.md) for more details. + +Also, you need to allow your frontend to access the configuration see : + +Example implementation would be in : `packages/app/src/config.d.ts` + +```typescript +export interface Config { + app: { + ... some code + badges: { + /** + * badges obfuscate + * @visibility frontend + */ + obfuscate?: string; + }; + }; +} +``` + +then include in the `packages/app/package.json` : + +```json +"files": [ + "dist", + "config.d.ts" + ], +"configSchema": "config.d.ts", +``` + ## Sample Badges Here are some samples of badges for the `artists-lookup` service in the Demo Backstage site: diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 0e736d1cbc..be2f7adc79 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.42-next.1", + "version": "0.2.42-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -50,14 +50,9 @@ "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", "@backstage/test-utils": "workspace:^", - "@testing-library/dom": "^8.0.0", - "@testing-library/jest-dom": "^5.10.1", - "@testing-library/react": "^12.1.3", - "@testing-library/user-event": "^14.0.0", + "@testing-library/jest-dom": "^5.16.5", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", - "cross-fetch": "^3.1.5", - "msw": "^1.0.0" + "cross-fetch": "^3.1.5" }, "files": [ "dist" diff --git a/plugins/badges/src/api/BadgesClient.ts b/plugins/badges/src/api/BadgesClient.ts index 9a89806d56..9a08db882a 100644 --- a/plugins/badges/src/api/BadgesClient.ts +++ b/plugins/badges/src/api/BadgesClient.ts @@ -18,31 +18,55 @@ import { generatePath } from 'react-router-dom'; import { ResponseError } from '@backstage/errors'; import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { BadgesApi, BadgeSpec } from './types'; -import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { ConfigApi, DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; export class BadgesClient implements BadgesApi { private readonly discoveryApi: DiscoveryApi; - private readonly identityApi: IdentityApi; + private readonly fetchApi: FetchApi; + private readonly configApi: ConfigApi; constructor(options: { discoveryApi: DiscoveryApi; - identityApi: IdentityApi; + fetchApi: FetchApi; + configApi: ConfigApi; }) { this.discoveryApi = options.discoveryApi; - this.identityApi = options.identityApi; + this.fetchApi = options.fetchApi; + this.configApi = options.configApi; + } + + static fromConfig(options: { + fetchApi: FetchApi; + discoveryApi: DiscoveryApi; + configApi: ConfigApi; + }) { + return new BadgesClient(options); } public async getEntityBadgeSpecs(entity: Entity): Promise { - const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity); - const { token } = await this.identityApi.getCredentials(); - const response = await fetch(entityBadgeSpecsUrl, { - headers: token - ? { - Authorization: `Bearer ${token}`, - } - : undefined, - }); + // Check if obfuscation is enabled in the configuration + const obfuscate = this.configApi.getOptionalBoolean('app.badges.obfuscate'); + if (obfuscate) { + const entityUuidUrl = await this.getEntityUuidUrl(entity); + const entityUuid = await this.getEntityUuid(entityUuidUrl).then(data => { + return data.uuid; + }); + const entityUuidBadgeSpecsUrl = await this.getEntityUuidBadgeSpecsUrl( + entityUuid, + ); + + const response = await this.fetchApi.fetch(entityUuidBadgeSpecsUrl); + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return await response.json(); + } + + // If obfuscation is disabled, get the badge specs directly as the previous implementation + const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity); + const response = await this.fetchApi.fetch(entityBadgeSpecsUrl); if (!response.ok) { throw await ResponseError.fromResponse(response); } @@ -50,14 +74,39 @@ export class BadgesClient implements BadgesApi { return await response.json(); } + private async getEntityUuidUrl(entity: Entity): Promise { + const routeParams = this.getEntityRouteParams(entity); + const path = generatePath(`:namespace/:kind/:name`, routeParams); + const baseUrl = await this.discoveryApi.getBaseUrl('badges'); + const obfuscatedEntityUrl = `${baseUrl}/entity/${path}/obfuscated`; + + return obfuscatedEntityUrl; + } + + private async getEntityUuid(entityUuidUrl: string): Promise { + const responseEntityUuid = await this.fetchApi.fetch(entityUuidUrl); + + if (!responseEntityUuid.ok) { + throw await ResponseError.fromResponse(responseEntityUuid); + } + return await responseEntityUuid.json(); + } + + private async getEntityUuidBadgeSpecsUrl(entityUuid: { + uuid: string; + }): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('badges'); + return `${baseUrl}/entity/${entityUuid}/badge-specs`; + } + private async getEntityBadgeSpecsUrl(entity: Entity): Promise { const routeParams = this.getEntityRouteParams(entity); const path = generatePath(`:namespace/:kind/:name`, routeParams); - return `${await this.discoveryApi.getBaseUrl( - 'badges', - )}/entity/${path}/badge-specs`; + const baseUrl = await this.discoveryApi.getBaseUrl('badges'); + return `${baseUrl}/entity/${path}/badge-specs`; } + // This function is used to generate the route parameters using the entity kind, namespace and name private getEntityRouteParams(entity: Entity) { return { kind: entity.kind.toLocaleLowerCase('en-US'), diff --git a/plugins/badges/src/plugin.ts b/plugins/badges/src/plugin.ts index 7ef3025458..771e3ed893 100644 --- a/plugins/badges/src/plugin.ts +++ b/plugins/badges/src/plugin.ts @@ -15,11 +15,12 @@ */ import { badgesApiRef, BadgesClient } from './api'; import { + configApiRef, createApiFactory, createComponentExtension, createPlugin, + fetchApiRef, discoveryApiRef, - identityApiRef, } from '@backstage/core-plugin-api'; /** @public */ @@ -28,9 +29,17 @@ export const badgesPlugin = createPlugin({ apis: [ createApiFactory({ api: badgesApiRef, - deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, - factory: ({ discoveryApi, identityApi }) => - new BadgesClient({ discoveryApi, identityApi }), + deps: { + fetchApi: fetchApiRef, + discoveryApi: discoveryApiRef, + configApi: configApiRef, + }, + factory: ({ fetchApi, discoveryApi, configApi }) => + new BadgesClient({ + fetchApi, + discoveryApi, + configApi, + }), }), ], }); diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index 298c684d3e..8f7de5e0f2 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-bazaar +## 0.2.8-next.2 + +### Patch Changes + +- 900880ab7c3: Fixed `validateDOMNesting` warnings +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/cli@0.22.7-next.0 + - @backstage/plugin-catalog@1.11.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/bazaar/dev/__fixtures__/get-projects-response.json b/plugins/bazaar/dev/__fixtures__/get-projects-response.json new file mode 100644 index 0000000000..7aee019a6e --- /dev/null +++ b/plugins/bazaar/dev/__fixtures__/get-projects-response.json @@ -0,0 +1,35 @@ +{ + "status": "ok", + "data": [ + { + "id": 10, + "entity_ref": null, + "title": "Lorem ipsum 1", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "status": "proposed", + "updated_at": "2023-03-10T12:41:41.941Z", + "community": "", + "size": "medium", + "start_date": null, + "end_date": null, + "responsible": "Responsible Adult 1", + "docs": null, + "members_count": "3" + }, + { + "id": 11, + "entity_ref": null, + "title": "Lorem ipsum 2", + "description": "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud", + "status": "proposed", + "updated_at": "2023-03-09T12:41:41.941Z", + "community": "", + "size": "medium", + "start_date": null, + "end_date": null, + "responsible": "Responsible Adult 2", + "docs": null, + "members_count": "5" + } + ] +} diff --git a/plugins/bazaar/dev/index.tsx b/plugins/bazaar/dev/index.tsx index a4509bc1fc..f2c40df8e1 100644 --- a/plugins/bazaar/dev/index.tsx +++ b/plugins/bazaar/dev/index.tsx @@ -16,10 +16,30 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { bazaarPlugin, BazaarPage } from '../src/plugin'; +import { bazaarApiRef } from '../src/api'; + +import getProjectsData from './__fixtures__/get-projects-response.json'; createDevApp() .registerPlugin(bazaarPlugin) + .registerApi({ + api: bazaarApiRef, + deps: {}, + factory: () => + ({ + getProjects: async () => getProjectsData, + } as any), + }) + .registerApi({ + api: catalogApiRef, + deps: {}, + factory: () => + ({ + getEntities: () => ({}), + } as CatalogApi), + }) .addPage({ element: , title: 'Root Page', diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index c61db47231..5e8bb28df6 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.2.8-next.1", + "version": "0.2.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx index 97b247e289..61d1dfebb9 100644 --- a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx +++ b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx @@ -94,11 +94,7 @@ export const ProjectCard = ({ setOpenCard(true)}> - {title} - - } + title={title} subtitle={`updated ${DateTime.fromISO( new Date(updatedAt!).toISOString(), ).toRelative({ diff --git a/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx b/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx index 8f7160600a..39319b6353 100644 --- a/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx +++ b/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx @@ -15,9 +15,14 @@ */ import React, { ChangeEvent, useState } from 'react'; -import { Content } from '@backstage/core-components'; import { ProjectCard } from '../ProjectCard/ProjectCard'; -import { makeStyles, Grid, TablePagination, GridSize } from '@material-ui/core'; +import { + Box, + makeStyles, + Grid, + TablePagination, + GridSize, +} from '@material-ui/core'; import { BazaarProject } from '../../types'; import { Entity } from '@backstage/catalog-model'; @@ -80,7 +85,7 @@ export const ProjectPreview = ({ } return ( - + {bazaarProjects .slice((page - 1) * rows, rows * page) @@ -101,6 +106,7 @@ export const ProjectPreview = ({ {useTablePagination && ( )} - + ); }; diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index 1ef42a6bbe..2fba6da9b3 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-bitrise +## 0.1.45-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.45-next.1 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 1b3740a058..1c1c3523ea 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.45-next.1", + "version": "0.1.45-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index f7ffc5e63c..886703f0c8 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.3-next.0 + - @backstage/config@1.0.7 + ## 0.1.19-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md index 491dbf497e..83b072061b 100644 --- a/plugins/catalog-backend-module-aws/api-report.md +++ b/plugins/catalog-backend-module-aws/api-report.md @@ -3,11 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AwsCredentialIdentity } from '@aws-sdk/types'; +import { AwsCredentialsManager } from '@backstage/integration-aws-node'; import { CatalogProcessor } from '@backstage/plugin-catalog-node'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-node'; import { CatalogProcessorParser } from '@backstage/plugin-catalog-node'; import { Config } from '@backstage/config'; -import { Credentials } from 'aws-sdk'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { LocationSpec } from '@backstage/plugin-catalog-common'; @@ -19,11 +20,16 @@ import { UrlReader } from '@backstage/backend-common'; // @public export type AWSCredentialFactory = ( awsAccountId: string, -) => Promise; +) => Promise; // @public export class AwsEKSClusterProcessor implements CatalogProcessor { - constructor(options: { credentialsFactory?: AWSCredentialFactory }); + constructor(options: { + credentialsFactory?: AWSCredentialFactory; + credentialsManager?: AwsCredentialsManager; + }); + // (undocumented) + static fromConfig(configRoot: Config): AwsEKSClusterProcessor; // (undocumented) getProcessorName(): string; // (undocumented) @@ -44,7 +50,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { options: { logger: Logger; }, - ): AwsOrganizationCloudAccountProcessor; + ): Promise; // (undocumented) getProcessorName(): string; // (undocumented) diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 73bd5123d2..50d7349cef 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", "description": "A Backstage catalog backend module that helps integrate towards AWS", - "version": "0.1.19-next.1", + "version": "0.1.19-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -45,7 +45,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@aws-sdk/client-s3": "^3.208.0", + "@aws-sdk/client-eks": "^3.310.0", + "@aws-sdk/client-organizations": "^3.310.0", + "@aws-sdk/client-s3": "^3.310.0", + "@aws-sdk/credential-providers": "^3.310.0", + "@aws-sdk/middleware-endpoint": "^3.310.0", + "@aws-sdk/types": "^3.310.0", "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", @@ -53,23 +58,23 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/integration-aws-node": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/types": "workspace:^", - "aws-sdk": "^2.840.0", "lodash": "^4.17.21", "p-limit": "^3.0.2", "uuid": "^8.0.0", "winston": "^3.2.1" }, "devDependencies": { - "@aws-sdk/util-stream-node": "^3.208.0", + "@aws-sdk/util-stream-node": "^3.310.0", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", "aws-sdk-client-mock": "^2.0.0", - "aws-sdk-mock": "^5.2.1", + "aws-sdk-client-mock-jest": "^2.0.0", "luxon": "^3.0.0", "yaml": "^2.0.0" }, diff --git a/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts b/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts deleted file mode 100644 index fded78aed4..0000000000 --- a/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import aws, { Credentials } from 'aws-sdk'; -import { CredentialsOptions } from 'aws-sdk/lib/credentials'; - -export class AwsCredentials { - /** - * If accessKeyId and secretAccessKey are missing, the DefaultAWSCredentialsProviderChain will be used: - * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html - */ - static create( - config: { - accessKeyId?: string; - secretAccessKey?: string; - roleArn?: string; - externalId?: string; - }, - roleSessionName: string, - ): Credentials | CredentialsOptions | undefined { - if (!config) { - return undefined; - } - - const accessKeyId = config.accessKeyId; - const secretAccessKey = config.secretAccessKey; - let explicitCredentials: Credentials | undefined; - - if (accessKeyId && secretAccessKey) { - explicitCredentials = new Credentials({ - accessKeyId, - secretAccessKey, - }); - } - - const roleArn = config.roleArn; - if (roleArn) { - return new aws.ChainableTemporaryCredentials({ - masterCredentials: explicitCredentials, - params: { - RoleArn: roleArn, - RoleSessionName: roleSessionName, - ExternalId: config.externalId, - }, - }); - } - - return explicitCredentials; - } -} diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts index ecf3b1a961..6cec6ed66d 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts @@ -15,24 +15,28 @@ */ import { AwsEKSClusterProcessor } from './AwsEKSClusterProcessor'; -import AWSMock from 'aws-sdk-mock'; -import aws from 'aws-sdk'; +import { mockClient } from 'aws-sdk-client-mock'; +import { + ListClustersResponse, + DescribeClusterResponse, + ListClustersCommand, + DescribeClusterCommand, + EKSClient, +} from '@aws-sdk/client-eks'; describe('AwsEKSClusterProcessor', () => { - AWSMock.setSDKInstance(aws); - describe('readLocation', () => { const processor = new (AwsEKSClusterProcessor as any)({}); const location = { type: 'aws-eks', target: '957140518395/us-west-2' }; const emit = jest.fn(); it('generates cluster correctly', async () => { - const clusters: aws.EKS.Types.ListClustersResponse = { + const clusters: ListClustersResponse = { clusters: ['backstage-test'], nextToken: undefined, }; - const cluster: aws.EKS.Types.DescribeClusterResponse = { + const cluster: DescribeClusterResponse = { cluster: { name: 'backstage-test', arn: 'arn:aws:1', @@ -42,9 +46,10 @@ describe('AwsEKSClusterProcessor', () => { }, }, }; + const mock = mockClient(EKSClient); - AWSMock.mock('EKS', 'listClusters', clusters); - AWSMock.mock('EKS', 'describeCluster', cluster); + mock.on(ListClustersCommand).resolves(clusters); + mock.on(DescribeClusterCommand).resolves(cluster); await processor.readLocation(location, false, emit); diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts index 05c85c7426..4a19b9eb7f 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts @@ -24,8 +24,14 @@ import { ANNOTATION_KUBERNETES_API_SERVER_CA, ANNOTATION_KUBERNETES_AUTH_PROVIDER, } from '@backstage/plugin-kubernetes-common'; -import { Credentials, EKS } from 'aws-sdk'; +import { EKS } from '@aws-sdk/client-eks'; import { AWSCredentialFactory } from '../types'; +import { AwsCredentialIdentity, Provider } from '@aws-sdk/types'; +import { + AwsCredentialsManager, + DefaultAwsCredentialsManager, +} from '@backstage/integration-aws-node'; +import { Config } from '@backstage/config'; const ACCOUNTID_ANNOTATION: string = 'amazonaws.com/account-id'; const ARN_ANNOTATION: string = 'amazonaws.com/arn'; @@ -39,9 +45,22 @@ const ARN_ANNOTATION: string = 'amazonaws.com/arn'; */ export class AwsEKSClusterProcessor implements CatalogProcessor { private credentialsFactory?: AWSCredentialFactory; + private credentialsManager?: AwsCredentialsManager; - constructor(options: { credentialsFactory?: AWSCredentialFactory }) { + static fromConfig(configRoot: Config): AwsEKSClusterProcessor { + const awsCredentaislManager = + DefaultAwsCredentialsManager.fromConfig(configRoot); + return new AwsEKSClusterProcessor({ + credentialsManager: awsCredentaislManager, + }); + } + + constructor(options: { + credentialsFactory?: AWSCredentialFactory; + credentialsManager?: AwsCredentialsManager; + }) { this.credentialsFactory = options.credentialsFactory; + this.credentialsManager = options.credentialsManager; } getProcessorName(): string { @@ -73,20 +92,30 @@ export class AwsEKSClusterProcessor implements CatalogProcessor { ); } - let credentials: Credentials | undefined; + let credentials: AwsCredentialIdentity | undefined; if (this.credentialsFactory) { credentials = await this.credentialsFactory(accountId); } - const eksClient = new EKS({ credentials, region }); - const clusters = await eksClient.listClusters({}).promise(); + let providerFunction: (() => Provider) | undefined; + if (this.credentialsManager) { + const credentialsProvider = + await this.credentialsManager.getCredentialProvider({ accountId }); + providerFunction = () => credentialsProvider.sdkCredentialProvider; + } + + const eksClient = new EKS({ + credentials, + credentialDefaultProvider: providerFunction, + }); + const clusters = await eksClient.listClusters({}); if (clusters.clusters === undefined) { return true; } const results = clusters.clusters - .map(cluster => eksClient.describeCluster({ name: cluster }).promise()) + .map(cluster => eksClient.describeCluster({ name: cluster })) .map(async describedClusterPromise => { const describedCluster = await describedClusterPromise; if (describedCluster.cluster) { diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts index b9b8ba7741..458a1f3531 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts @@ -14,7 +14,13 @@ * limitations under the License. */ +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; import { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor'; +import { + ListAccountsCommand, + OrganizationsClient, +} from '@aws-sdk/client-organizations'; describe('AwsOrganizationCloudAccountProcessor', () => { describe('readLocation', () => { @@ -23,26 +29,21 @@ describe('AwsOrganizationCloudAccountProcessor', () => { }); const location = { type: 'aws-cloud-accounts', target: '' }; const emit = jest.fn(); - const listAccounts = jest.fn(); + const mock = mockClient(OrganizationsClient); - processor.organizations.listAccounts = listAccounts; - afterEach(() => jest.resetAllMocks()); + afterEach(() => { + jest.resetAllMocks(); + }); it('generates component entities for accounts', async () => { - listAccounts.mockImplementation(() => { - return { - async promise() { - return { - Accounts: [ - { - Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', - Name: 'testaccount', - }, - ], - NextToken: undefined, - }; + mock.on(ListAccountsCommand).resolves({ + Accounts: [ + { + Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', + Name: 'testaccount', }, - }; + ], + NextToken: undefined, }); await processor.readLocation(location, false, emit); expect(emit).toHaveBeenCalledWith({ @@ -74,24 +75,18 @@ describe('AwsOrganizationCloudAccountProcessor', () => { type: 'aws-cloud-accounts', target: 'o-1vl18kc5a3', }; - listAccounts.mockImplementation(() => { - return { - async promise() { - return { - Accounts: [ - { - Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', - Name: 'testaccount', - }, - { - Arn: 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395', - Name: 'testaccount2', - }, - ], - NextToken: undefined, - }; + mock.on(ListAccountsCommand).resolves({ + Accounts: [ + { + Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395', + Name: 'testaccount', }, - }; + { + Arn: 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395', + Name: 'testaccount2', + }, + ], + NextToken: undefined, }); await processor.readLocation(locationTest, false, emit); expect(emit).toHaveBeenCalledTimes(1); diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts index c0ba1e3e98..264fa7a50d 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts @@ -22,13 +22,17 @@ import { processingResult, } from '@backstage/plugin-catalog-node'; import { LocationSpec } from '@backstage/plugin-catalog-common'; -import AWS, { Credentials, Organizations } from 'aws-sdk'; -import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations'; -import { Logger } from 'winston'; import { - AwsOrganizationProviderConfig, - readAwsOrganizationConfig, -} from '../awsOrganization/config'; + Account, + ListAccountsResponse, + Organizations, +} from '@aws-sdk/client-organizations'; +import { Logger } from 'winston'; +import { readAwsOrganizationConfig } from '../awsOrganization/config'; +import { + AwsCredentialProvider, + DefaultAwsCredentialsManager, +} from '@backstage/integration-aws-node'; const AWS_ORGANIZATION_REGION = 'us-east-1'; const LOCATION_TYPE = 'aws-cloud-accounts'; @@ -47,39 +51,31 @@ const ORGANIZATION_ANNOTATION = 'amazonaws.com/organization-id'; */ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { private readonly organizations: Organizations; - private readonly provider: AwsOrganizationProviderConfig; + private readonly logger: Logger; - static fromConfig(config: Config, options: { logger: Logger }) { + static async fromConfig(config: Config, options: { logger: Logger }) { const c = config.getOptionalConfig('catalog.processors.awsOrganization'); - return new AwsOrganizationCloudAccountProcessor({ - ...options, - provider: c ? readAwsOrganizationConfig(c) : {}, + const orgConfig = c ? readAwsOrganizationConfig(c) : undefined; + const awsCredentialsManager = + DefaultAwsCredentialsManager.fromConfig(config); + const credProvider = await awsCredentialsManager.getCredentialProvider({ + arn: orgConfig?.roleArn, }); - } - - private static buildCredentials( - config: AwsOrganizationProviderConfig, - ): Credentials | undefined { - const roleArn = config.roleArn; - if (!roleArn) { - return undefined; - } - - return new AWS.ChainableTemporaryCredentials({ - params: { - RoleSessionName: 'backstage-aws-organization-processor', - RoleArn: roleArn, - }, - }); - } - - private constructor(options: { provider: AwsOrganizationProviderConfig }) { - this.provider = options.provider; - const credentials = AwsOrganizationCloudAccountProcessor.buildCredentials( - this.provider, + return new AwsOrganizationCloudAccountProcessor( + credProvider, + options.logger, ); - this.organizations = new AWS.Organizations({ - credentials, + } + + private constructor( + private readonly credProvider: AwsCredentialProvider, + logger: Logger, + ) { + this.logger = logger?.child({ + target: this.getProcessorName(), + }); + this.organizations = new Organizations({ + credentialDefaultProvider: () => this.credProvider.sdkCredentialProvider, region: AWS_ORGANIZATION_REGION, }); // Only available in us-east-1 } @@ -97,6 +93,8 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { return false; } + this.logger?.info('Discovering AWS Organization Account objects'); + (await this.getAwsAccounts()) .map(account => this.mapAccountToComponent(account)) .filter(entity => { @@ -143,9 +141,8 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { let nextToken = undefined; while (isInitialAttempt || nextToken) { isInitialAttempt = false; - const orgAccounts: ListAccountsResponse = await this.organizations - .listAccounts({ NextToken: nextToken }) - .promise(); + const orgAccounts: ListAccountsResponse = + await this.organizations.listAccounts({ NextToken: nextToken }); if (orgAccounts.Accounts) { awsAccounts = awsAccounts.concat(orgAccounts.Accounts); } diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts index 821fb6d4f4..e9e17ad76a 100644 --- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts +++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts @@ -23,8 +23,9 @@ import { import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { AwsS3EntityProvider } from './AwsS3EntityProvider'; -import aws from 'aws-sdk'; -import AWSMock from 'aws-sdk-mock'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; +import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'; class PersistingTaskRunner implements TaskRunner { private tasks: TaskInvocationDefinition[] = []; @@ -42,38 +43,39 @@ class PersistingTaskRunner implements TaskRunner { const logger = getVoidLogger(); describe('AwsS3EntityProvider', () => { - AWSMock.setSDKInstance(aws); - const createObjectList = (keys: string[]): aws.S3.ObjectList => { + const createObjectList = (keys: string[]) => { const objects = keys.map(key => { return { Key: key, - } as aws.S3.Types.Object; + }; }); - - return objects as aws.S3.ObjectList; + return objects; }; const keys = ['key1.yaml', 'key2.yaml', 'key3.yaml', 'key 4.yaml']; + const mock = mockClient(S3Client); - AWSMock.mock('S3', 'listObjectsV2', async req => { - const prefix = req.Prefix ?? ''; + beforeEach(() => { + mock.on(ListObjectsV2Command).callsFake(async req => { + const prefix = req.Prefix ?? ''; + + if (!req.ContinuationToken) { + return { + Contents: createObjectList( + keys + .slice(0, Math.ceil(keys.length / 2)) + .map(key => `${prefix}${key}`), + ), + NextContinuationToken: 'next-token', + }; + } - if (!req.ContinuationToken) { return { Contents: createObjectList( - keys - .slice(0, Math.ceil(keys.length / 2)) - .map(key => `${prefix}${key}`), + keys.slice(Math.ceil(keys.length / 2)).map(key => `${prefix}${key}`), ), - NextContinuationToken: 'next-token', - } as aws.S3.Types.ListObjectsV2Output; - } - - return { - Contents: createObjectList( - keys.slice(Math.ceil(keys.length / 2)).map(key => `${prefix}${key}`), - ), - } as aws.S3.Types.ListObjectsV2Output; + }; + }); }); afterEach(() => { @@ -103,7 +105,9 @@ describe('AwsS3EntityProvider', () => { }); const schedulingConfig: Record = {}; - + const normalizedExpectedBaseUrl = expectedBaseUrl.endsWith('/') + ? expectedBaseUrl + : `${expectedBaseUrl}/`; const schedule = new PersistingTaskRunner(); const entityProviderConnection: EntityProviderConnection = { applyMutation: jest.fn(), @@ -131,7 +135,7 @@ describe('AwsS3EntityProvider', () => { await (taskDef.fn as () => Promise)(); const expectedEntities = keys.map(key => { - const url = encodeURI(`${expectedBaseUrl}${key}`); + const url = encodeURI(`${normalizedExpectedBaseUrl}${key}`); return { entity: { apiVersion: 'backstage.io/v1alpha1', @@ -161,6 +165,25 @@ describe('AwsS3EntityProvider', () => { // eslint-disable-next-line jest/expect-expect it('apply full update on scheduled execution', async () => { + return expectMutation( + 'regionalStatic', + { + bucketName: 'bucket-1', + prefix: 'sub/dir/', + region: 'eu-west-1', + }, + 'https://bucket-1.s3.eu-west-1.amazonaws.com/sub/dir/', + { + 'key1.yaml': 'generated-8ece85ad90200c6577b99f553dcbedde05fa34bb', + 'key2.yaml': 'generated-6b54c6aaa44696f5e91ce0f54fb27bf837549d11', + 'key3.yaml': 'generated-88c703cf1aa66913db4033b029adc0b174574646', + 'key 4.yaml': 'generated-2b7e068bb4ec818c14f179a1e721843fc2dbc5f9', + }, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('apply full update on scheduled execution force path style', async () => { return expectMutation( 'regionalStatic', { @@ -175,11 +198,72 @@ describe('AwsS3EntityProvider', () => { 'key3.yaml': 'generated-8d75f78ed9fa618ce433b226dc24eeab441f3a2d', 'key 4.yaml': 'generated-1e0249dcb5805fc2ce6ac2d3c4d2a3ef4f1270c0', }, + { + s3ForcePathStyle: true, + }, ); }); // eslint-disable-next-line jest/expect-expect - it('us-east-1 has region-less URLs', async () => { + it('apply full update on scheduled execution no prefix', async () => { + return expectMutation( + 'regionalStatic', + { + bucketName: 'bucket-1', + region: 'eu-west-1', + }, + 'https://bucket-1.s3.eu-west-1.amazonaws.com/', + { + 'key1.yaml': 'generated-ac6d99e56e0fbd6bf3719a5182f37bfa84b1cddc', + 'key2.yaml': 'generated-cc3e1145496cd95bf3898687bb5b7d2991a7b7fd', + 'key3.yaml': 'generated-a197557b0cb89d14ac1297231ed930bd4c40b664', + 'key 4.yaml': 'generated-a774e6c1d1c7ee511b7d79ebb99b08a39cf439c4', + }, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('apply full update on scheduled execution force path style no prefix', async () => { + return expectMutation( + 'regionalStatic', + { + bucketName: 'bucket-1', + region: 'eu-west-1', + }, + 'https://s3.eu-west-1.amazonaws.com/bucket-1', + { + 'key1.yaml': 'generated-b390cd4de7ff00f6d51426870fc0e685029fbf09', + 'key2.yaml': 'generated-15ebeb1e53ffe71ea93f037076aefcdc5cc5e617', + 'key3.yaml': 'generated-fd0a33e70bafd707a60139decbd0383361ab3b4e', + 'key 4.yaml': 'generated-ac574fb266d9c8bd983b25a208c538e4ad2c1d06', + }, + { + s3ForcePathStyle: true, + }, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('apply full update on scheduled execution bucket with dot in it', async () => { + return expectMutation( + 'regionalStatic', + { + bucketName: 'bucket.1', + prefix: 'sub/dir/', + region: 'eu-west-1', + }, + 'https://s3.eu-west-1.amazonaws.com/bucket.1/sub/dir/', + { + 'key1.yaml': 'generated-651b5851d4670b7d5bb9a319d8f6eb295092e4ad', + 'key2.yaml': 'generated-ccddcdc3ad814aea072883118f3a9d0ac39a161e', + 'key3.yaml': 'generated-b2b8548903aa764544ab79dc81ab5093e6e4ba83', + 'key 4.yaml': 'generated-aafa9cc0422f5e8ccce1616dc0c74fa94f692af3', + }, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('us-east-1 does not have region-less URLs', async () => { return expectMutation( 'usEast1', { @@ -187,12 +271,12 @@ describe('AwsS3EntityProvider', () => { prefix: 'sub/dir/', region: 'us-east-1', }, - 'https://s3.amazonaws.com/bucket-1/sub/dir/', + 'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/', { - 'key1.yaml': 'generated-f7e3f1c89f62cbf0d82db16452faaa7c040fc331', - 'key2.yaml': 'generated-925173f7dba1acaa73cac5ef4d10c3ed7660aa25', - 'key3.yaml': 'generated-d94cf017911ed7fb3be4a62a3dae1f5202879de3', - 'key 4.yaml': 'generated-f917fca0cfacc2be478ca2a3cff94d00917a1cde', + 'key1.yaml': 'generated-980e6ad47fbfbfeead708a9c7c87331b7540296a', + 'key2.yaml': 'generated-266794d8e789089dddba2b42cd79e70b149aa61c', + 'key3.yaml': 'generated-96f0cdcd7e33aa687c19d160ec7d5b1975cb9ea1', + 'key 4.yaml': 'generated-4f6d3ffb232e4a5b82c3a52549e4c36dab1c2f8d', }, ); }); @@ -209,18 +293,60 @@ describe('AwsS3EntityProvider', () => { bucketName: 'bucket-1', prefix: 'sub/dir/', }, - 'https://s3.eu-central-1.amazonaws.com/bucket-1/sub/dir/', + 'https://bucket-1.s3.eu-central-1.amazonaws.com/sub/dir/', { - 'key1.yaml': 'generated-285d144b5c1e24e801b97f61f794d566dabc7236', - 'key2.yaml': 'generated-36b78d7fa69690059797d4cc4f40c2a5eaed1d6d', - 'key3.yaml': 'generated-a39ad804bf65993cbc0f3810ff128a3c91daf768', - 'key 4.yaml': 'generated-bd7622ac86c313cb812d90e335b2f5f6bff14670', + 'key1.yaml': 'generated-bb17a9f9b420edd93137692f000b2d443a29239c', + 'key2.yaml': 'generated-b4559ecc8b615407e421dbaa3fcb9338e977b8ac', + 'key3.yaml': 'generated-bb3d500a6f8b351b0eed1aec77ebce65c1ab41fb', + 'key 4.yaml': 'generated-686de87f5882f6558b43280c5b6047e485199e88', }, ); }); // eslint-disable-next-line jest/expect-expect it('custom endpoint', async () => { + return expectMutation( + 'customEndpoint', + { + bucketName: 'bucket-1', + prefix: 'sub/dir/', + }, + 'http://bucket-1.localhost:1234/sub/dir/', + { + 'key1.yaml': 'generated-79625971ead9c39c91b0bfef4d7c748325e31234', + 'key2.yaml': 'generated-eeae95f16605d3afdc615436c18c9fbd35781299', + 'key3.yaml': 'generated-692976561563c3fa55889ad090363716c7b7e915', + 'key 4.yaml': 'generated-c8f92ea0eb96c355b7e56cbc461cef29f830747e', + }, + { + endpoint: 'http://localhost:1234/', + }, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('custom endpoint bucket dot', async () => { + return expectMutation( + 'customEndpoint', + { + bucketName: 'bucket.1', + prefix: 'sub/dir/', + }, + 'http://localhost:1234/bucket.1/sub/dir/', + { + 'key1.yaml': 'generated-07be77ac8626ac76a924414e0edf5ce66d3e0b27', + 'key2.yaml': 'generated-47295ada3c894cf9bac0d570a34d909244ea2f74', + 'key3.yaml': 'generated-30e98f3ebd350f3b618acd024199b5f31545495d', + 'key 4.yaml': 'generated-b71bc7661b92cef0db62c9652912fd5e4b245aa7', + }, + { + endpoint: 'http://localhost:1234/', + }, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('custom endpoint force path style', async () => { return expectMutation( 'customEndpoint', { @@ -235,7 +361,8 @@ describe('AwsS3EntityProvider', () => { 'key 4.yaml': 'generated-659f17a2429f107db04850f9081e3e9c56084aeb', }, { - endpoint: 'http://localhost:1234', + endpoint: 'http://localhost:1234/', + s3ForcePathStyle: true, }, ); }); @@ -305,12 +432,12 @@ describe('AwsS3EntityProvider', () => { }, }, }, - 'https://s3.eu-west-1.amazonaws.com/bucket-1/sub/dir/', + 'https://bucket-1.s3.eu-west-1.amazonaws.com/sub/dir/', { - 'key1.yaml': 'generated-7f6d5861b0b3401a38b5fe62e6c7ca11da5fd6d8', - 'key2.yaml': 'generated-a290be145586042af7d80715626399c9d661718d', - 'key3.yaml': 'generated-8d75f78ed9fa618ce433b226dc24eeab441f3a2d', - 'key 4.yaml': 'generated-1e0249dcb5805fc2ce6ac2d3c4d2a3ef4f1270c0', + 'key1.yaml': 'generated-8ece85ad90200c6577b99f553dcbedde05fa34bb', + 'key2.yaml': 'generated-6b54c6aaa44696f5e91ce0f54fb27bf837549d11', + 'key3.yaml': 'generated-88c703cf1aa66913db4033b029adc0b174574646', + 'key 4.yaml': 'generated-2b7e068bb4ec818c14f179a1e721843fc2dbc5f9', }, undefined, true, diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts index 8212f9279a..4a87a42e75 100644 --- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts +++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts @@ -23,13 +23,20 @@ import { locationSpecToLocationEntity, } from '@backstage/plugin-catalog-node'; import { LocationSpec } from '@backstage/plugin-catalog-common'; -import { AwsCredentials } from '../credentials/AwsCredentials'; import { readAwsS3Configs } from './config'; import { AwsS3Config } from './types'; -import { S3 } from 'aws-sdk'; -import { ListObjectsV2Output } from 'aws-sdk/clients/s3'; +import { + ListObjectsV2Command, + ListObjectsV2Output, + S3, +} from '@aws-sdk/client-s3'; import * as uuid from 'uuid'; import { Logger } from 'winston'; +import { getEndpointFromInstructions } from '@aws-sdk/middleware-endpoint'; +import { + AwsCredentialsManager, + DefaultAwsCredentialsManager, +} from '@backstage/integration-aws-node'; // TODO: event-based updates using S3 events (+ queue like SQS)? /** @@ -41,9 +48,10 @@ import { Logger } from 'winston'; */ export class AwsS3EntityProvider implements EntityProvider { private readonly logger: Logger; - private readonly s3: S3; + private s3?: S3; private readonly scheduleFn: () => Promise; private connection?: EntityProviderConnection; + private endpoint?: string; static fromConfig( configRoot: Config, @@ -78,7 +86,8 @@ export class AwsS3EntityProvider implements EntityProvider { `No schedule provided neither via code nor config for awsS3-provider:${providerConfig.id}.`, ); } - + const awsCredentialsManager = + DefaultAwsCredentialsManager.fromConfig(configRoot); const taskRunner = options.schedule ?? options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!); @@ -86,6 +95,7 @@ export class AwsS3EntityProvider implements EntityProvider { return new AwsS3EntityProvider( providerConfig, integration, + awsCredentialsManager, options.logger, taskRunner, ); @@ -94,7 +104,8 @@ export class AwsS3EntityProvider implements EntityProvider { private constructor( private readonly config: AwsS3Config, - integration: AwsS3Integration, + private readonly integration: AwsS3Integration, + private readonly awsCredentialsManager: AwsCredentialsManager, logger: Logger, taskRunner: TaskRunner, ) { @@ -102,17 +113,6 @@ export class AwsS3EntityProvider implements EntityProvider { target: this.getProviderName(), }); - this.s3 = new S3({ - apiVersion: '2006-03-01', - credentials: AwsCredentials.create( - integration.config, - 'backstage-aws-s3-provider', - ), - endpoint: integration.config.endpoint, - region: this.config.region, - s3ForcePathStyle: integration.config.s3ForcePathStyle, - }); - this.scheduleFn = this.createScheduleFn(taskRunner); } @@ -146,6 +146,28 @@ export class AwsS3EntityProvider implements EntityProvider { /** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */ async connect(connection: EntityProviderConnection): Promise { this.connection = connection; + const credProvider = + await this.awsCredentialsManager.getCredentialProvider(); + this.s3 = new S3({ + apiVersion: '2006-03-01', + credentialDefaultProvider: () => credProvider.sdkCredentialProvider, + endpoint: this.integration.config.endpoint, + region: this.config.region, + forcePathStyle: this.integration.config.s3ForcePathStyle, + }); + + // https://github.com/aws/aws-sdk-js-v3/issues/4122#issuecomment-1298968804 + const endpoint = await getEndpointFromInstructions( + { + Bucket: this.config.bucketName, + }, + ListObjectsV2Command, + this.s3.config as unknown as Record, + ); + if (endpoint?.url) + this.endpoint = endpoint.url.href.endsWith('/') + ? endpoint.url.href + : `${endpoint.url.href}/`; await this.scheduleFn(); } @@ -175,18 +197,21 @@ export class AwsS3EntityProvider implements EntityProvider { } private async listAllObjectKeys(): Promise { + if (!this.s3) { + throw new Error('Not initialized'); + } + const keys: string[] = []; let continuationToken: string | undefined = undefined; let output: ListObjectsV2Output; do { - const request = this.s3.listObjectsV2({ + output = await this.s3.listObjectsV2({ Bucket: this.config.bucketName, ContinuationToken: continuationToken, Prefix: this.config.prefix, }); - output = await request.promise(); if (output.Contents) { output.Contents.forEach(item => { if (item.Key && !item.Key.endsWith('/')) { @@ -209,9 +234,6 @@ export class AwsS3EntityProvider implements EntityProvider { } private createObjectUrl(key: string): string { - const bucketName = this.config.bucketName; - const endpoint = this.s3.endpoint.href; - - return encodeURI(`${endpoint}${bucketName}/${key}`); + return new URL(key, this.endpoint).href; } } diff --git a/plugins/catalog-backend-module-aws/src/types.ts b/plugins/catalog-backend-module-aws/src/types.ts index 3d9df38181..23460c104e 100644 --- a/plugins/catalog-backend-module-aws/src/types.ts +++ b/plugins/catalog-backend-module-aws/src/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Credentials } from 'aws-sdk'; +import { AwsCredentialIdentity } from '@aws-sdk/types'; /** * A factory for providing user-specified AWS credentials for a given AWS account. @@ -22,4 +22,4 @@ import { Credentials } from 'aws-sdk'; */ export type AWSCredentialFactory = ( awsAccountId: string, -) => Promise; +) => Promise; diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index f54a55f9e9..9e3b24071f 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-github +## 0.3.0-next.2 + +### Minor Changes + +- 970678adbe2: Implement events support for `GithubMultiOrgEntityProvider` + + **BREAKING:** Passing in a custom `teamTransformer` will now correctly completely override the default transformer behavior + +### Patch Changes + +- bd101cefd37: Updated the `team.edited` event emitted from `GithubOrgEntityProvider` to also include teams description. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/config@1.0.7 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/api-report.md b/plugins/catalog-backend-module-github/api-report.md index 0698fae8a0..7b944b33a9 100644 --- a/plugins/catalog-backend-module-github/api-report.md +++ b/plugins/catalog-backend-module-github/api-report.md @@ -10,6 +10,7 @@ import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; +import { EventBroker } from '@backstage/plugin-events-node'; import { EventParams } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; import { GithubCredentialsProvider } from '@backstage/integration'; @@ -158,6 +159,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { // @public export interface GithubMultiOrgEntityProviderOptions { + eventBroker?: EventBroker; githubCredentialsProvider?: GithubCredentialsProvider; githubUrl: string; id: string; diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index 5b29baa05c..1f79fea531 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-github", "description": "A Backstage catalog backend module that helps integrate towards GitHub", - "version": "0.2.8-next.1", + "version": "0.3.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index df4565a174..7a0a29e098 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -21,6 +21,7 @@ import { graphql as graphqlMsw } from 'msw'; import { setupServer } from 'msw/node'; import { TeamTransformer, UserTransformer } from './defaultTransformers'; import { + getOrganizationsFromUser, getOrganizationTeams, getOrganizationUsers, getTeamMembers, @@ -448,6 +449,37 @@ describe('github', () => { }); }); + describe('getOrganizationsFromUser', () => { + it('reads orgs from user', async () => { + const input: QueryResponse = { + user: { + organizations: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'a', + }, + { + login: 'b', + }, + { + login: 'c', + }, + ], + }, + }, + }; + + server.use( + graphqlMsw.query('orgs', (_req, res, ctx) => res(ctx.data(input))), + ); + + await expect(getOrganizationsFromUser(graphql, 'foo')).resolves.toEqual({ + orgs: ['a', 'b', 'c'], + }); + }); + }); + describe('getTeamMembers', () => { it('reads team members', async () => { const input: QueryResponse = { diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index d0b629092a..412b751c38 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -33,6 +33,7 @@ import { DeferredEntity } from '@backstage/plugin-catalog-node'; export type QueryResponse = { organization?: OrganizationResponse; repositoryOwner?: RepositoryOwnerResponse; + user?: UserResponse; }; type RepositoryOwnerResponse = { @@ -46,11 +47,19 @@ export type OrganizationResponse = { repositories?: Connection; }; +export type UserResponse = { + organizations?: Connection; +}; + export type PageInfo = { hasNextPage: boolean; endCursor?: string; }; +export type GithubOrg = { + login: string; +}; + /** * Github User * @@ -334,6 +343,34 @@ export async function getOrganizationTeamsFromUsers( return { groups }; } +export async function getOrganizationsFromUser( + client: typeof graphql, + user: string, +): Promise<{ + orgs: string[]; +}> { + const query = ` + query orgs($user: String!) { + user(login: $user) { + organizations(first: 100) { + nodes { login } + pageInfo { hasNextPage, endCursor } + } + } + }`; + + const orgs = await queryWithPaging( + client, + query, + '', + r => r.user?.organizations, + async o => o.login, + { user }, + ); + + return { orgs }; +} + export async function getOrganizationTeam( client: typeof graphql, org: string, diff --git a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts index 76ab55552d..99bef6e074 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts @@ -16,11 +16,13 @@ import { getVoidLogger } from '@backstage/backend-common'; import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; import { GithubCredentialsProvider, GithubIntegrationConfig, } from '@backstage/integration'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; +import { EventSubscriber } from '@backstage/plugin-events-node'; import { graphql } from '@octokit/graphql'; import { GithubMultiOrgEntityProvider, @@ -160,7 +162,7 @@ describe('GithubMultiOrgEntityProvider', () => { const logger = getVoidLogger(); const gitHubConfig: GithubIntegrationConfig = { - host: 'https://github.com', + host: 'github.com', }; const mockGetCredentials = jest.fn().mockReturnValue({ @@ -201,9 +203,9 @@ describe('GithubMultiOrgEntityProvider', () => { metadata: { annotations: { 'backstage.io/managed-by-location': - 'url:https://https://github.com/a', + 'url:https://github.com/a', 'backstage.io/managed-by-origin-location': - 'url:https://https://github.com/a', + 'url:https://github.com/a', 'github.com/user-login': 'a', }, description: 'c', @@ -227,9 +229,9 @@ describe('GithubMultiOrgEntityProvider', () => { metadata: { annotations: { 'backstage.io/managed-by-location': - 'url:https://https://github.com/x', + 'url:https://github.com/x', 'backstage.io/managed-by-origin-location': - 'url:https://https://github.com/x', + 'url:https://github.com/x', 'github.com/user-login': 'x', }, description: 'z', @@ -253,9 +255,9 @@ describe('GithubMultiOrgEntityProvider', () => { metadata: { annotations: { 'backstage.io/managed-by-location': - 'url:https://https://github.com/q', + 'url:https://github.com/q', 'backstage.io/managed-by-origin-location': - 'url:https://https://github.com/q', + 'url:https://github.com/q', 'github.com/user-login': 'q', }, description: 's', @@ -279,9 +281,9 @@ describe('GithubMultiOrgEntityProvider', () => { metadata: { annotations: { 'backstage.io/managed-by-location': - 'url:https://https://github.com/orgs/orgA/teams/team', + 'url:https://github.com/orgs/orgA/teams/team', 'backstage.io/managed-by-origin-location': - 'url:https://https://github.com/orgs/orgA/teams/team', + 'url:https://github.com/orgs/orgA/teams/team', 'github.com/team-slug': 'orgA/team', }, namespace: 'orga', @@ -308,9 +310,9 @@ describe('GithubMultiOrgEntityProvider', () => { metadata: { annotations: { 'backstage.io/managed-by-location': - 'url:https://https://github.com/orgs/orgB/teams/team', + 'url:https://github.com/orgs/orgB/teams/team', 'backstage.io/managed-by-origin-location': - 'url:https://https://github.com/orgs/orgB/teams/team', + 'url:https://github.com/orgs/orgB/teams/team', 'github.com/team-slug': 'orgB/team', }, namespace: 'orgb', @@ -467,7 +469,7 @@ describe('GithubMultiOrgEntityProvider', () => { const logger = getVoidLogger(); const gitHubConfig: GithubIntegrationConfig = { - host: 'https://github.com', + host: 'github.com', }; const mockGetCredentials = jest.fn().mockReturnValue({ @@ -507,9 +509,9 @@ describe('GithubMultiOrgEntityProvider', () => { metadata: { annotations: { 'backstage.io/managed-by-location': - 'url:https://https://github.com/a', + 'url:https://github.com/a', 'backstage.io/managed-by-origin-location': - 'url:https://https://github.com/a', + 'url:https://github.com/a', 'github.com/user-login': 'a', }, description: 'c', @@ -533,9 +535,9 @@ describe('GithubMultiOrgEntityProvider', () => { metadata: { annotations: { 'backstage.io/managed-by-location': - 'url:https://https://github.com/x', + 'url:https://github.com/x', 'backstage.io/managed-by-origin-location': - 'url:https://https://github.com/x', + 'url:https://github.com/x', 'github.com/user-login': 'x', }, description: 'z', @@ -559,9 +561,9 @@ describe('GithubMultiOrgEntityProvider', () => { metadata: { annotations: { 'backstage.io/managed-by-location': - 'url:https://https://github.com/q', + 'url:https://github.com/q', 'backstage.io/managed-by-origin-location': - 'url:https://https://github.com/q', + 'url:https://github.com/q', 'github.com/user-login': 'q', }, description: 's', @@ -585,9 +587,9 @@ describe('GithubMultiOrgEntityProvider', () => { metadata: { annotations: { 'backstage.io/managed-by-location': - 'url:https://https://github.com/orgs/orgC/teams/team', + 'url:https://github.com/orgs/orgC/teams/team', 'backstage.io/managed-by-origin-location': - 'url:https://https://github.com/orgs/orgC/teams/team', + 'url:https://github.com/orgs/orgC/teams/team', 'github.com/team-slug': 'orgC/team', }, namespace: 'orgc', @@ -614,9 +616,9 @@ describe('GithubMultiOrgEntityProvider', () => { metadata: { annotations: { 'backstage.io/managed-by-location': - 'url:https://https://github.com/orgs/orgD/teams/team', + 'url:https://github.com/orgs/orgD/teams/team', 'backstage.io/managed-by-origin-location': - 'url:https://https://github.com/orgs/orgD/teams/team', + 'url:https://github.com/orgs/orgD/teams/team', 'github.com/team-slug': 'orgD/team', }, namespace: 'orgd', @@ -713,4 +715,1223 @@ describe('GithubMultiOrgEntityProvider', () => { }); }); }); + + describe('events', () => { + let onEvent: Function; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + beforeEach(() => { + const logger = getVoidLogger(); + const config = new ConfigReader({ + integrations: { + github: [ + { + host: 'github.com', + }, + ], + }, + }); + + const mockGetCredentials = jest.fn().mockReturnValue({ + headers: { token: 'blah' }, + type: 'app', + }); + + const githubCredentialsProvider: GithubCredentialsProvider = { + getCredentials: mockGetCredentials, + }; + + const mockEventBroker = { + publish: async () => {}, + subscribe: (subscriber: EventSubscriber) => { + onEvent = subscriber.onEvent; + }, + }; + + const entityProvider = GithubMultiOrgEntityProvider.fromConfig(config, { + id: 'my-id', + githubCredentialsProvider, + githubUrl: 'https://github.com', + logger, + orgs: ['orgA', 'orgB'], + eventBroker: mockEventBroker, + }); + + entityProvider.connect(entityProviderConnection); + }); + + afterEach(() => jest.resetAllMocks()); + + it('should ignore events from non-applicable orgs', async () => { + await onEvent({ + topic: 'github.organization', + eventPayload: { + action: 'member_added', + membership: { + user: { + name: 'githubuser', + login: 'githubuser', + avatar_url: 'https://avatars.githubusercontent.com/u/83820368', + email: 'user1@test.com', + }, + }, + organization: { + login: 'test-org', + }, + }, + }); + + expect(entityProviderConnection.applyMutation).not.toHaveBeenCalled(); + + await onEvent({ + topic: 'github.installation', + eventPayload: { + action: 'created', + installation: { + account: { + login: 'test-org', + }, + }, + }, + }); + + expect(entityProviderConnection.applyMutation).not.toHaveBeenCalled(); + }); + + describe('installation', () => { + it('adds users and groups from new org', async () => { + const mockClient = jest.fn(); + + mockClient + .mockResolvedValueOnce({ + organization: { + membersWithRole: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'a', + name: 'b', + bio: 'c', + email: 'd', + avatarUrl: 'e', + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgB/team', + name: 'TeamB', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + parentTeam: { + slug: 'parent', + combinedSlug: '', + members: { pageInfo: { hasNextPage: false }, nodes: [] }, + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a' }], + }, + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgA/team', + name: 'TeamA', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + parentTeam: { + slug: 'parent', + combinedSlug: '', + members: { pageInfo: { hasNextPage: false }, nodes: [] }, + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a' }], + }, + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgB/team', + name: 'TeamB', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + parentTeam: { + slug: 'parent', + combinedSlug: '', + members: { pageInfo: { hasNextPage: false }, nodes: [] }, + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a' }], + }, + }, + ], + }, + }, + }); + + (graphql.defaults as jest.Mock).mockReturnValue(mockClient); + + await onEvent({ + topic: 'github.installation', + eventPayload: { + action: 'created', + installation: { + account: { + login: 'orgB', + }, + }, + }, + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/a', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/a', + 'github.com/user-login': 'a', + }, + description: 'c', + name: 'a', + }, + spec: { + memberOf: ['orga/team', 'orgb/team'], + profile: { + displayName: 'b', + email: 'd', + picture: 'e', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/orgs/orgB/teams/team', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/orgs/orgB/teams/team', + 'github.com/team-slug': 'orgB/team', + }, + namespace: 'orgb', + name: 'team', + description: 'The one and only team', + }, + spec: { + children: [], + parent: 'parent', + profile: { + displayName: 'TeamB', + picture: 'http://example.com/team.jpeg', + }, + type: 'team', + members: ['default/a'], + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + ], + removed: [], + }); + }); + }); + + describe('organization', () => { + it('should add a new user', async () => { + const mockClient = jest.fn(); + + mockClient + .mockResolvedValueOnce({ + user: { + organizations: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'orgA', + }, + { + login: 'orgB', + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgA/team', + name: 'TeamB', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a', name: 'a' }], + }, + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [], + }, + }, + }); + + (graphql.defaults as jest.Mock).mockReturnValue(mockClient); + + await onEvent({ + topic: 'github.organization', + eventPayload: { + action: 'member_added', + organization: { + login: 'orgB', + }, + membership: { + user: { + name: 'a', + avatar_url: 'https://avatars.githubusercontent.com/u/83820368', + email: 'user1@test.com', + login: 'a', + }, + }, + }, + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/a', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/a', + 'github.com/user-login': 'a', + }, + name: 'a', + }, + spec: { + memberOf: ['orga/team'], + profile: { + displayName: 'a', + email: 'user1@test.com', + picture: 'https://avatars.githubusercontent.com/u/83820368', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + ], + removed: [], + }); + }); + + it('should remove a user', async () => { + const mockClient = jest.fn(); + + mockClient.mockResolvedValueOnce({ + user: { + organizations: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'orgC', + }, + ], + }, + }, + }); + + (graphql.defaults as jest.Mock).mockReturnValue(mockClient); + + await onEvent({ + topic: 'github.organization', + eventPayload: { + action: 'member_removed', + organization: { + login: 'orgB', + }, + membership: { + user: { + name: 'a', + avatar_url: 'https://avatars.githubusercontent.com/u/83820368', + email: 'user1@test.com', + login: 'a', + }, + }, + }, + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [], + removed: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/a', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/a', + 'github.com/user-login': 'a', + }, + name: 'a', + }, + spec: { + memberOf: [], + profile: { + displayName: 'a', + email: 'user1@test.com', + picture: 'https://avatars.githubusercontent.com/u/83820368', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + ], + }); + }); + + it('should only update group memberships of a user that still belongs to an applicable org', async () => { + const mockClient = jest.fn(); + + mockClient + .mockResolvedValueOnce({ + user: { + organizations: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'orgA', + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgA/team', + name: 'TeamB', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a', name: 'a' }], + }, + }, + ], + }, + }, + }); + + (graphql.defaults as jest.Mock).mockReturnValue(mockClient); + + await onEvent({ + topic: 'github.organization', + eventPayload: { + action: 'member_removed', + organization: { + login: 'orgB', + }, + membership: { + user: { + name: 'a', + avatar_url: 'https://avatars.githubusercontent.com/u/83820368', + email: 'user1@test.com', + login: 'a', + }, + }, + }, + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/a', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/a', + 'github.com/user-login': 'a', + }, + name: 'a', + }, + spec: { + memberOf: ['orga/team'], + profile: { + displayName: 'a', + email: 'user1@test.com', + picture: 'https://avatars.githubusercontent.com/u/83820368', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + ], + removed: [], + }); + }); + }); + + describe('team', () => { + it('should create a new group from a new team', async () => { + await onEvent({ + topic: 'github.team', + eventPayload: { + action: 'created', + organization: { + login: 'orgB', + }, + team: { + name: 'New Team', + slug: 'new-team', + description: 'description from the new team', + html_url: 'https://github.com/orgs/orgB/teams/new-team', + parent: { + slug: 'father-team', + }, + }, + }, + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'new-team', + namespace: 'orgb', + description: 'description from the new team', + annotations: { + 'backstage.io/edit-url': + 'https://github.com/orgs/orgB/teams/new-team/edit', + 'backstage.io/managed-by-location': + 'url:https://github.com/orgs/orgB/teams/new-team', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/orgs/orgB/teams/new-team', + 'github.com/team-slug': 'orgB/new-team', + }, + }, + spec: { + type: 'team', + children: [], + members: [], + parent: 'father-team', + profile: { + displayName: 'New Team', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + ], + removed: [], + }); + }); + + it('should remove a group from a deleted team', async () => { + await onEvent({ + topic: 'github.team', + eventPayload: { + action: 'deleted', + organization: { + login: 'orgB', + }, + team: { + name: 'New Team', + slug: 'new-team', + description: 'description from the new team', + html_url: 'https://github.com/orgs/orgB/teams/new-team', + parent: { + slug: 'father-team', + }, + }, + }, + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [], + removed: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'new-team', + namespace: 'orgb', + description: 'description from the new team', + annotations: { + 'backstage.io/edit-url': + 'https://github.com/orgs/orgB/teams/new-team/edit', + 'backstage.io/managed-by-location': + 'url:https://github.com/orgs/orgB/teams/new-team', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/orgs/orgB/teams/new-team', + 'github.com/team-slug': 'orgB/new-team', + }, + }, + spec: { + type: 'team', + children: [], + members: [], + parent: 'father-team', + profile: { + displayName: 'New Team', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + ], + }); + }); + + it('should update group and user entities when a team is edited', async () => { + const mockClient = jest.fn(); + + mockClient + .mockResolvedValueOnce({ + organization: { + team: { + slug: 'team', + combinedSlug: 'orgA/team', + name: 'TeamA', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a', name: 'a' }], + }, + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + membersWithRole: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'a', + name: 'b', + bio: 'c', + email: 'd', + avatarUrl: 'e', + }, + { + login: 'w', + name: 'x', + bio: 'y', + email: 'z', + avatarUrl: 'q', + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgA/team', + name: 'TeamA', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a', name: 'a' }], + }, + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgB/team', + name: 'TeamB', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a', name: 'a' }], + }, + }, + ], + }, + }, + }); + + (graphql.defaults as jest.Mock).mockReturnValue(mockClient); + + await onEvent({ + topic: 'github.team', + eventPayload: { + action: 'edited', + changes: { + name: { + from: 'oldName', + }, + description: { + from: 'oldDescription', + }, + }, + team: { + slug: 'team', + parent: { + slug: 'parent', + }, + }, + organization: { + login: 'orgA', + }, + }, + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/a', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/a', + 'github.com/user-login': 'a', + }, + name: 'a', + description: 'c', + }, + spec: { + memberOf: ['orga/team', 'orgb/team'], + profile: { + displayName: 'b', + email: 'd', + picture: 'e', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/edit-url': 'https://example.com', + 'backstage.io/managed-by-location': + 'url:https://github.com/orgs/orgA/teams/team', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/orgs/orgA/teams/team', + 'github.com/team-slug': 'orgA/team', + }, + namespace: 'orga', + name: 'team', + description: 'The one and only team', + }, + spec: { + children: [], + parent: 'parent', + profile: { + displayName: 'TeamA', + picture: 'http://example.com/team.jpeg', + }, + type: 'team', + members: ['default/a'], + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + ], + removed: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/orgs/orgA/teams/oldname', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/orgs/orgA/teams/oldname', + 'github.com/team-slug': 'orgA/oldname', + }, + namespace: 'orga', + name: 'oldname', + description: 'oldDescription', + }, + spec: { + children: [], + parent: 'parent', + profile: { + displayName: 'oldName', + }, + type: 'team', + members: [], + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + ], + }); + }); + }); + + describe('membership', () => { + it('should update group and user entities when a member is added', async () => { + const mockClient = jest.fn(); + + mockClient + .mockResolvedValueOnce({ + organization: { + team: { + slug: 'team', + combinedSlug: 'orgA/team', + name: 'TeamA', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a', name: 'a' }], + }, + }, + }, + }) + .mockResolvedValueOnce({ + user: { + organizations: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'orgA', + }, + { + login: 'orgB', + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgA/team', + name: 'TeamA', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a', name: 'a' }], + }, + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgB/team', + name: 'TeamB', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a', name: 'a' }], + }, + }, + ], + }, + }, + }); + + (graphql.defaults as jest.Mock).mockReturnValue(mockClient); + + await onEvent({ + topic: 'github.membership', + eventPayload: { + action: 'added', + organization: { + login: 'orgA', + }, + team: { + slug: 'teama', + }, + member: { + name: 'a', + login: 'a', + avatar_url: 'https://avatars.githubusercontent.com/u/83820368', + email: 'user1@test.com', + }, + }, + }); + await new Promise(process.nextTick); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/a', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/a', + 'github.com/user-login': 'a', + }, + name: 'a', + }, + spec: { + memberOf: ['orga/team', 'orgb/team'], + profile: { + displayName: 'a', + email: 'user1@test.com', + picture: 'https://avatars.githubusercontent.com/u/83820368', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/edit-url': 'https://example.com', + 'backstage.io/managed-by-location': + 'url:https://github.com/orgs/orgA/teams/team', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/orgs/orgA/teams/team', + 'github.com/team-slug': 'orgA/team', + }, + namespace: 'orga', + name: 'team', + description: 'The one and only team', + }, + spec: { + children: [], + parent: 'parent', + profile: { + displayName: 'TeamA', + picture: 'http://example.com/team.jpeg', + }, + type: 'team', + members: ['default/a'], + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + ], + removed: [], + }); + }); + + it('should update group and user entities when a member is removed', async () => { + const mockClient = jest.fn(); + + mockClient + .mockResolvedValueOnce({ + organization: { + team: { + slug: 'team', + combinedSlug: 'orgA/team', + name: 'TeamA', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [], + }, + }, + }, + }) + .mockResolvedValueOnce({ + user: { + organizations: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'orgA', + }, + { + login: 'orgB', + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgA/team', + name: 'TeamA', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [], + }, + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'orgB/team', + name: 'TeamB', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'https://example.com', + parentTeam: { + slug: 'parent', + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'a', name: 'a' }], + }, + }, + ], + }, + }, + }); + + (graphql.defaults as jest.Mock).mockReturnValue(mockClient); + + await onEvent({ + topic: 'github.membership', + eventPayload: { + action: 'removed', + organization: { + login: 'orgA', + }, + team: { + slug: 'teama', + }, + member: { + name: 'a', + login: 'a', + avatar_url: 'https://avatars.githubusercontent.com/u/83820368', + email: 'user1@test.com', + }, + }, + }); + await new Promise(process.nextTick); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/a', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/a', + 'github.com/user-login': 'a', + }, + name: 'a', + }, + spec: { + memberOf: ['orgb/team'], + profile: { + displayName: 'a', + email: 'user1@test.com', + picture: 'https://avatars.githubusercontent.com/u/83820368', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/edit-url': 'https://example.com', + 'backstage.io/managed-by-location': + 'url:https://github.com/orgs/orgA/teams/team', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/orgs/orgA/teams/team', + 'github.com/team-slug': 'orgA/team', + }, + namespace: 'orga', + name: 'team', + description: 'The one and only team', + }, + spec: { + children: [], + parent: 'parent', + profile: { + displayName: 'TeamA', + picture: 'http://example.com/team.jpeg', + }, + type: 'team', + members: [], + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, + ], + removed: [], + }); + }); + }); + }); }); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts index ce45f30d96..51796c2116 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts @@ -21,7 +21,9 @@ import { DEFAULT_NAMESPACE, Entity, GroupEntity, + parseEntityRef, stringifyEntityRef, + UserEntity, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -32,10 +34,24 @@ import { ScmIntegrations, } from '@backstage/integration'; import { + DeferredEntity, EntityProvider, EntityProviderConnection, } from '@backstage/plugin-catalog-node'; +import { EventBroker, EventParams } from '@backstage/plugin-events-node'; import { graphql } from '@octokit/graphql'; +import { + InstallationCreatedEvent, + InstallationEvent, + OrganizationEvent, + OrganizationMemberAddedEvent, + OrganizationMemberRemovedEvent, + MembershipEvent, + TeamCreatedEvent, + TeamDeletedEvent, + TeamEditedEvent, + TeamEvent, +} from '@octokit/webhooks-types'; import { merge } from 'lodash'; import * as uuid from 'uuid'; import { Logger } from 'winston'; @@ -44,6 +60,7 @@ import { assignGroupsToUsers, buildOrgHierarchy, defaultOrganizationTeamTransformer, + defaultUserTransformer, getOrganizationTeams, getOrganizationUsers, GithubTeam, @@ -55,6 +72,11 @@ import { ANNOTATION_GITHUB_TEAM_SLUG, ANNOTATION_GITHUB_USER_LOGIN, } from '../lib/annotation'; +import { + getOrganizationsFromUser, + getOrganizationTeam, + getOrganizationTeamsFromUsers, +} from '../lib/github'; import { splitTeamSlug } from '../lib/util'; /** @@ -118,8 +140,18 @@ export interface GithubMultiOrgEntityProviderOptions { * By default groups will be namespaced according to their GitHub org. */ teamTransformer?: TeamTransformer; + + /** + * An EventBroker to subscribe this provider to GitHub events to trigger delta mutations + */ + eventBroker?: EventBroker; } +type CreateDeltaOperation = (entities: Entity[]) => { + added: DeferredEntity[]; + removed: DeferredEntity[]; +}; + /** * Ingests org data (users and groups) from GitHub. * @@ -161,6 +193,13 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { provider.schedule(options.schedule); + if (options.eventBroker) { + options.eventBroker.subscribe({ + supportsEventTopics: provider.supportsEventTopics.bind(provider), + onEvent: provider.onEvent.bind(provider), + }); + } + return provider; } @@ -271,6 +310,476 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { markCommitComplete(); } + private supportsEventTopics(): string[] { + return [ + 'github.installation', + 'github.organization', + 'github.team', + 'github.membership', + ]; + } + + private async onEvent(params: EventParams): Promise { + const { logger } = this.options; + logger.debug(`Received event from ${params.topic}`); + + const orgs = this.options.orgs?.length + ? this.options.orgs + : await this.getAllOrgs(this.options.gitHubConfig); + + const eventPayload = params.eventPayload as + | InstallationEvent + | OrganizationEvent + | MembershipEvent + | TeamEvent; + + if ( + !orgs.includes( + (eventPayload as InstallationEvent).installation?.account?.login, + ) && + !orgs.includes( + (eventPayload as OrganizationEvent | MembershipEvent | TeamEvent) + .organization?.login, + ) + ) { + return; + } + + // https://docs.github.com/webhooks-and-events/webhooks/webhook-events-and-payloads#installation + if ( + params.topic.includes('installation') && + eventPayload.action === 'created' + ) { + // We can only respond to installation.created events to add new users/groups since a + // installation.deleted event won't provide us info on what user/groups we should remove and + // we can't query the uninstalled org since we will no longer have access. This will need to be + // eventually resolved via occasional full mutation runs by calling read() + await this.onInstallationChange( + eventPayload as InstallationCreatedEvent, + orgs, + ); + } + + // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#organization + if ( + params.topic.includes('organization') && + (eventPayload.action === 'member_added' || + eventPayload.action === 'member_removed') + ) { + await this.onMemberChangeInOrganization(eventPayload, orgs); + } + + // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#team + if (params.topic.includes('team')) { + if ( + eventPayload.action === 'created' || + eventPayload.action === 'deleted' + ) { + await this.onTeamChangeInOrganization( + eventPayload as TeamCreatedEvent | TeamDeletedEvent, + ); + } else if (eventPayload.action === 'edited') { + await this.onTeamEditedInOrganization( + eventPayload as TeamEditedEvent, + orgs, + ); + } + } + + // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership + if (params.topic.includes('membership')) { + this.onMembershipChangedInTeam(eventPayload as MembershipEvent, orgs); + } + + return; + } + + private async onInstallationChange( + event: InstallationCreatedEvent, + applicableOrgs: string[], + ) { + if (!this.connection) { + throw new Error('Not initialized'); + } + + const org = event.installation.account.login; + const { headers, type: tokenType } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${org}`, + }); + const client = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers, + }); + + const { users } = await getOrganizationUsers( + client, + org, + tokenType, + this.options.userTransformer, + ); + + const { groups } = await getOrganizationTeams( + client, + org, + this.defaultMultiOrgTeamTransformer.bind(this), + ); + + // Fetch group memberships of users in case they already exist and + // have memberships in groups from other applicable orgs + for (const userOrg of applicableOrgs) { + const { headers: orgHeaders } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${userOrg}`, + }); + const orgClient = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers: orgHeaders, + }); + + const { groups: userGroups } = await getOrganizationTeamsFromUsers( + orgClient, + userOrg, + users.map( + u => + u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] || + u.metadata.name, + ), + this.defaultMultiOrgTeamTransformer.bind(this), + ); + + assignGroupsToUsers(users, userGroups); + } + + const { added, removed } = this.createAddEntitiesOperation([ + ...users, + ...groups, + ]); + await this.connection.applyMutation({ + type: 'delta', + removed, + added, + }); + } + + private async onMemberChangeInOrganization( + event: OrganizationMemberAddedEvent | OrganizationMemberRemovedEvent, + applicableOrgs: string[], + ) { + if (!this.connection) { + throw new Error('Not initialized'); + } + + const userTransformer = + this.options.userTransformer || defaultUserTransformer; + const { name, avatar_url: avatarUrl, email, login } = event.membership.user; + const org = event.organization.login; + const { headers } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${org}`, + }); + const client = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers, + }); + + const { orgs } = await getOrganizationsFromUser(client, login); + const userApplicableOrgs = orgs.filter(o => applicableOrgs.includes(o)); + + let updateMemberships: boolean; + let createDeltaOperation: CreateDeltaOperation; + if (event.action === 'member_removed') { + if (userApplicableOrgs.length) { + // If the user is still associated with another applicable org then we don't want to remove + // them, just update the entity to remove any potential group memberships from the old org + createDeltaOperation = this.createAddEntitiesOperation.bind(this); + updateMemberships = true; + } else { + // User is no longer part of any applicable orgs so we can remove it, + // no need to take memberships into account + createDeltaOperation = this.createRemoveEntitiesOperation.bind(this); + updateMemberships = false; + } + } else { + // We're not sure if this user was already added as part of another applicable org + // so grab the latest memberships (potentially from teams of other orgs) to ensure + // we're not accidentally omitting them + createDeltaOperation = this.createAddEntitiesOperation.bind(this); + updateMemberships = true; + } + + const user = (await userTransformer( + { + name, + avatarUrl, + login, + email: email ?? undefined, + }, + { + org, + client, + query: '', + }, + )) as UserEntity; + + if (updateMemberships) { + for (const userOrg of userApplicableOrgs) { + const { headers: orgHeaders } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${userOrg}`, + }); + const orgClient = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers: orgHeaders, + }); + + const { groups } = await getOrganizationTeamsFromUsers( + orgClient, + userOrg, + [login], + this.defaultMultiOrgTeamTransformer.bind(this), + ); + + assignGroupsToUsers([user], groups); + } + } + + const { added, removed } = createDeltaOperation([user]); + await this.connection.applyMutation({ + type: 'delta', + removed, + added, + }); + } + + private async onTeamChangeInOrganization( + event: TeamCreatedEvent | TeamDeletedEvent, + ) { + if (!this.connection) { + throw new Error('Not initialized'); + } + + const org = event.organization.login; + const { headers } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${org}`, + }); + const client = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers, + }); + + const { name, html_url: url, description, slug } = event.team; + const group = (await this.defaultMultiOrgTeamTransformer( + { + name, + slug, + editTeamUrl: `${url}/edit`, + combinedSlug: `${org}/${slug}`, + description: description ?? undefined, + parentTeam: { slug: event.team?.parent?.slug || '' } as GithubTeam, + // entity will be removed or is new + members: [], + }, + { + org, + client, + query: '', + }, + )) as Entity; + + const createDeltaOperation = + event.action === 'created' + ? this.createAddEntitiesOperation.bind(this) + : this.createRemoveEntitiesOperation.bind(this); + const { added, removed } = createDeltaOperation([group]); + + await this.connection.applyMutation({ + type: 'delta', + removed, + added, + }); + } + + private async onTeamEditedInOrganization( + event: TeamEditedEvent, + applicableOrgs: string[], + ) { + if (!this.connection) { + throw new Error('Not initialized'); + } + + const org = event.organization.login; + const { headers, type: tokenType } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${org}`, + }); + const client = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers, + }); + + const teamSlug = event.team.slug; + const { group } = await getOrganizationTeam( + client, + org, + teamSlug, + this.defaultMultiOrgTeamTransformer.bind(this), + ); + + const { users } = await getOrganizationUsers( + client, + org, + tokenType, + this.options.userTransformer, + ); + + const usersFromChangedGroup = + group.spec.members?.map(m => + stringifyEntityRef(parseEntityRef(m, { defaultKind: 'user' })), + ) || []; + const usersToRebuild = users.filter(u => + usersFromChangedGroup.includes(stringifyEntityRef(u)), + ); + + // Update memberships of associated members of this group in case the group entity ref changed + for (const userOrg of applicableOrgs) { + const { headers: orgHeaders } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${userOrg}`, + }); + const orgClient = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers: orgHeaders, + }); + + const { groups } = await getOrganizationTeamsFromUsers( + orgClient, + userOrg, + usersToRebuild.map( + u => + u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] || + u.metadata.name, + ), + this.defaultMultiOrgTeamTransformer.bind(this), + ); + + assignGroupsToUsers(usersToRebuild, groups); + } + + const oldName = event.changes.name?.from || ''; + const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-'); + const oldGroup = (await this.defaultMultiOrgTeamTransformer( + { + name: event.changes.name?.from, + slug: oldSlug, + combinedSlug: `${org}/${oldSlug}`, + description: event.changes.description?.from, + parentTeam: { slug: event.team?.parent?.slug || '' } as GithubTeam, + // entity will be removed + members: [], + }, + { + org, + client, + query: '', + }, + )) as Entity; + + // Remove the old group entity in case the entity ref is now different + const { removed } = this.createRemoveEntitiesOperation([oldGroup]); + const { added } = this.createAddEntitiesOperation([ + ...usersToRebuild, + group, + ]); + await this.connection.applyMutation({ + type: 'delta', + removed, + added, + }); + } + + private async onMembershipChangedInTeam( + event: MembershipEvent, + applicableOrgs: string[], + ) { + if (!this.connection) { + throw new Error('Not initialized'); + } + + // The docs are saying I will receive the slug for the removed event, + // but the types don't reflect that, + // so I will just check to be sure the slug is there + // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership + if (!('slug' in event.team)) { + return; + } + + const org = event.organization.login; + const { headers } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${org}`, + }); + const client = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers, + }); + + const teamSlug = event.team.slug; + const { group } = await getOrganizationTeam( + client, + org, + teamSlug, + this.defaultMultiOrgTeamTransformer.bind(this), + ); + + const userTransformer = + this.options.userTransformer || defaultUserTransformer; + const { name, avatar_url: avatarUrl, email, login } = event.member; + const user = (await userTransformer( + { + name, + avatarUrl, + login, + email: email ?? undefined, + }, + { + org, + client, + query: '', + }, + )) as UserEntity; + + const { orgs } = await getOrganizationsFromUser(client, login); + const userApplicableOrgs = orgs.filter(o => applicableOrgs.includes(o)); + for (const userOrg of userApplicableOrgs) { + const { headers: orgHeaders } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${userOrg}`, + }); + const orgClient = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers: orgHeaders, + }); + + const { groups } = await getOrganizationTeamsFromUsers( + orgClient, + userOrg, + [login], + this.defaultMultiOrgTeamTransformer.bind(this), + ); + + assignGroupsToUsers([user], groups); + } + + const { added, removed } = this.createAddEntitiesOperation([user, group]); + await this.connection.applyMutation({ + type: 'delta', + removed, + added, + }); + } + private schedule(schedule: GithubMultiOrgEntityProviderOptions['schedule']) { if (!schedule || schedule === 'manual') { return; @@ -301,9 +810,11 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { team: GithubTeam, ctx: TransformerContext, ): Promise { - const result = this.options.teamTransformer - ? await this.options.teamTransformer(team, ctx) - : await defaultOrganizationTeamTransformer(team, ctx); + if (this.options.teamTransformer) { + return await this.options.teamTransformer(team, ctx); + } + + const result = await defaultOrganizationTeamTransformer(team, ctx); if (result) { result.metadata.namespace = ctx.org.toLocaleLowerCase('en-US'); @@ -333,6 +844,32 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { ) .filter(Boolean) as string[]; } + + private createAddEntitiesOperation(entities: Entity[]) { + return { + removed: [], + added: entities.map(entity => ({ + locationKey: `github-multi-org-provider:${this.options.id}`, + entity: withLocations( + `https://${this.options.gitHubConfig.host}`, + entity, + ), + })), + }; + } + + private createRemoveEntitiesOperation(entities: Entity[]) { + return { + added: [], + removed: entities.map(entity => ({ + locationKey: `github-multi-org-provider:${this.options.id}`, + entity: withLocations( + `https://${this.options.gitHubConfig.host}`, + entity, + ), + })), + }; + } } // Helps wrap the timing and logging behaviors diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts index ff42e42718..3e58c1473f 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts @@ -833,6 +833,7 @@ describe('GithubOrgEntityProvider', () => { 'url:https://github.com/orgs/backstage/teams/mygroup-with-spaces', }, name: 'mygroup-with-spaces', + description: 'description-from-the-new-team', }, apiVersion: 'backstage.io/v1alpha1', kind: 'Group', diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts index 02e225c938..c181510a67 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts @@ -371,14 +371,21 @@ export class GithubOrgEntityProvider assignGroupsToUsers(usersToRebuild, groups); buildOrgHierarchy(groups); - const oldName = event.changes.name?.from || ''; + const oldName = event.changes.name?.from || event.team.name; const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-'); + const oldDescription = + event.changes.description?.from || event.team.description; + const oldDescriptionSlug = oldDescription + ?.toLowerCase() + .replaceAll(/\s/gi, '-'); + const { removed } = createDeltaOperation(org, [ { ...group, metadata: { name: oldSlug, + description: oldDescriptionSlug, }, }, ]); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index 8f8c67895c..46932c95c6 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -128,10 +128,11 @@ export class GitLabClient { throw new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`); } memberIds.push( - ...response.data.group.groupMembers.nodes.map( - (node: { user: { id: string } }) => + ...response.data.group.groupMembers.nodes + .filter(n => n.user) + .map(node => Number(node.user.id.replace(/^gid:\/\/gitlab\/User\//, '')), - ), + ), ); ({ hasNextPage, endCursor } = response.data.group.groupMembers.pageInfo); } while (hasNextPage); diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index 9921b0dabc..8c7b041d06 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.3.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/config@1.0.7 + ## 0.3.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index 686c89cd25..69bc2dfbd6 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", "description": "An entity provider for streaming large asset sources into the catalog", - "version": "0.3.2-next.1", + "version": "0.3.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 5fc587a4f9..345032bda7 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.11-next.2 + +### Patch Changes + +- accaceadffa: Fixed bug in `jsonSchemaRefPlaceholderResolver` where relative $ref files were resolved through file system instead of base URL of file +- Updated dependencies + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/config@1.0.7 + ## 0.1.11-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index e02490ef59..c9960d15f1 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", - "version": "0.1.11-next.1", + "version": "0.1.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts index 64c772b91e..59f7e5791f 100644 --- a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts @@ -65,6 +65,6 @@ export async function bundleFileWithRefs( }, }; const fileObject = parse(fileWithRefs); - const bundledObject = await $RefParser.bundle(fileObject, options); + const bundledObject = await $RefParser.bundle(baseUrl, fileObject, options); return stringify(bundledObject); } diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index 39bd1e3cdc..b5f13f39cd 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.1.2-next.2 + +### Patch Changes + +- 95b2168d71b: Fixes import paths and updates documentation +- Updated dependencies + - @backstage/config@1.0.7 + ## 0.1.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/README.md b/plugins/catalog-backend-module-puppetdb/README.md index d4782c6d18..9319299a06 100644 --- a/plugins/catalog-backend-module-puppetdb/README.md +++ b/plugins/catalog-backend-module-puppetdb/README.md @@ -31,9 +31,9 @@ Update the catalog plugin initialization in your backend to add the provider and + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: { minutes: 10 }, + timeout: { minutes: 50 }, -+ initialDelay: { seconds: 15} ++ initialDelay: { seconds: 15 } + }), -+ }); ++ }) + ); ``` @@ -67,7 +67,7 @@ of overriding the default transformer. export const customResourceTransformer: ResourceTransformer = async ( node, config, -): Promise => { +): Promise => { // Transformations may change namespace, owner, change entity naming pattern, add labels, annotations, etc. // Create the Resource Entity on your own, or wrap the default transformer @@ -77,14 +77,16 @@ export const customResourceTransformer: ResourceTransformer = async ( 2. Configure the provider with the transformer: -```ts -const puppetDbEntityProvider = PuppetDbEntityProvider.fromConfig(env.config, { - logger: env.logger, - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 10 }, - timeout: { minutes: 50 }, - initialDelay: { seconds: 15 }, - }), - transformer: customResourceTransformer, -}); +```diff + builder.addEntityProvider( + PuppetDbEntityProvider.fromConfig(env.config, { + logger: env.logger, ++ transformer: customResourceTransformer, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: { minutes: 10 }, + timeout: { minutes: 50 }, + initialDelay: { seconds: 15 } + }), + }) + ); ``` diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index 65bda327dd..3d1734657b 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", - "version": "0.1.2-next.1", + "version": "0.1.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts index ad89e1fd9a..42091fd6d6 100644 --- a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts @@ -28,7 +28,7 @@ import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, ResourceEntity, -} from '@backstage/catalog-model/'; +} from '@backstage/catalog-model'; import { DEFAULT_ENTITY_OWNER, ENDPOINT_NODES } from '../puppet/constants'; jest.mock('../puppet/read', () => { diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts index 8c63baf467..a5286a9d70 100644 --- a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts +++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts @@ -31,7 +31,7 @@ import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, Entity, -} from '@backstage/catalog-model/'; +} from '@backstage/catalog-model'; import { merge } from 'lodash'; import { readPuppetNodes } from '../puppet/read'; import { ENDPOINT_NODES } from '../puppet/constants'; diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts index 568ea7224e..ab3e85370a 100644 --- a/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts +++ b/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts @@ -16,7 +16,7 @@ import { PuppetDbEntityProviderConfig } from '../providers'; import { PuppetNode, ResourceTransformer } from './types'; -import { ResourceEntity } from '@backstage/catalog-model/'; +import { ResourceEntity } from '@backstage/catalog-model'; import { defaultResourceTransformer } from './transformers'; import fetch from 'node-fetch'; import { ResponseError } from '@backstage/errors'; diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 1a715e26bc..78021149ff 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-backend +## 1.9.1-next.2 + +### Patch Changes + +- ce8d203235b: Ensure that entity cache state is only written to the database when actually changed +- 485a6c5f7b5: Internal refactoring for performance in the service handlers +- 3587a968dcd: Fixed a bug in the `queryEntities` endpoint that was causing filtered entities to be included in cursor requests. +- 12a345317ab: Remove unnecessary join in the entity facets endpoint, exclude nulls +- Updated dependencies + - @backstage/plugin-scaffolder-common@1.3.0-next.0 + - @backstage/config@1.0.7 + ## 1.9.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 289a9a801d..13f358f1ff 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "1.9.1-next.1", + "version": "1.9.1-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts b/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts index 920acae7bd..eeab85f59e 100644 --- a/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts +++ b/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts @@ -33,13 +33,16 @@ export async function deleteOrphanedEntities(options: { // Limit iterations for sanity for (let i = 0; i < 100; ++i) { const candidates = await tx - .with('orphans', orphans => + .with('orphans', ['entity_id', 'entity_ref'], orphans => orphans - .from('refresh_state') - .select('entity_id', 'entity_ref') - .whereNotIn('entity_ref', keep => - keep.distinct('target_entity_ref').from('refresh_state_references'), - ), + .from('refresh_state') + .select('refresh_state.entity_id', 'refresh_state.entity_ref') + .leftOuterJoin( + 'refresh_state_references', + 'refresh_state_references.target_entity_ref', + 'refresh_state.entity_ref', + ) + .whereNull('refresh_state_references.target_entity_ref'), ) .select({ entityId: 'orphans.entity_id', diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index ab1e0b96c9..8e2f8b2f6f 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -150,7 +150,10 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { track.markProcessorsCompleted(result); if (result.ok) { - if (stableStringify(state) !== stableStringify(result.state)) { + const { ttl: _, ...stateWithoutTtl } = state ?? {}; + if ( + stableStringify(stateWithoutTtl) !== stableStringify(result.state) + ) { await this.processingDatabase.transaction(async tx => { await this.processingDatabase.updateEntityCache(tx, { id, @@ -314,7 +317,9 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { try { await this.processingDatabase.transaction(async tx => { const n = await this.processingDatabase.deleteOrphanedEntities(tx); - this.logger.info(`Deleted ${n} orphaned entities`); + if (n > 0) { + this.logger.info(`Deleted ${n} orphaned entities`); + } }); } catch (error) { this.logger.warn(`Failed to delete orphaned entities`, error); diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 08ae295834..4f011813e6 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -1418,6 +1418,87 @@ describe('DefaultEntitiesCatalog', () => { }, ); + it.each(databases.eachSupportedId())( + 'should exclude filtered entities when paginating, %p', + async databaseId => { + await createDatabase(databaseId); + + await Promise.all([ + addEntityToSearch(entityFrom('AA', { uid: '1', kind: 'included' })), + addEntityToSearch( + entityFrom('AA', { + namespace: 'namespace2', + kind: 'included', + uid: '2', + }), + ), + addEntityToSearch( + entityFrom('AA', { + namespace: 'ns', + kind: 'excluded', + uid: '3', + }), + ), + addEntityToSearch( + entityFrom('AA', { + namespace: 'namespace3', + uid: '4', + kind: 'included', + }), + ), + addEntityToSearch( + entityFrom('AA', { + namespace: 'namespace4', + uid: '5', + kind: 'included', + }), + ), + addEntityToSearch(entityFrom('CC', { uid: '6', kind: 'included' })), + addEntityToSearch(entityFrom('DD', { uid: '7', kind: 'included' })), + ]); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); + + const limit = 2; + + // initial request + const request1: QueryEntitiesInitialRequest = { + limit, + filter: { + key: 'kind', + values: ['included'], + }, + orderFields: [{ field: 'metadata.name', order: 'asc' }], + }; + const response1 = await catalog.queryEntities(request1); + expect(response1.items).toMatchObject([ + entityFrom('AA', { uid: '1', kind: 'included' }), + entityFrom('AA', { uid: '2', kind: 'included' }), + ]); + expect(response1.pageInfo.nextCursor).toBeDefined(); + expect(response1.pageInfo.prevCursor).toBeUndefined(); + expect(response1.totalItems).toBe(6); + + // second request (forward) + const request2: QueryEntitiesCursorRequest = { + cursor: response1.pageInfo.nextCursor!, + limit, + }; + const response2 = await catalog.queryEntities(request2); + expect(response2.items).toMatchObject([ + entityFrom('AA', { uid: '4', kind: 'included' }), + entityFrom('AA', { uid: '5', kind: 'included' }), + ]); + expect(response2.pageInfo.nextCursor).toBeDefined(); + expect(response2.pageInfo.prevCursor).toBeDefined(); + expect(response2.totalItems).toBe(6); + }, + ); + it.each(databases.eachSupportedId())( 'should paginate results without sort fields, %p', async databaseId => { @@ -1648,6 +1729,17 @@ describe('DefaultEntitiesCatalog', () => { ], }, }); + + await expect( + catalog.facets({ + facets: ['kind'], + filter: { not: { key: 'metadata.name', values: ['two'] } }, + }), + ).resolves.toEqual({ + facets: { + kind: [{ value: 'k', count: 1 }], + }, + }); }, ); @@ -1754,11 +1846,12 @@ function entityFrom( uid, namespace, title, - }: { uid?: string; namespace?: string; title?: string } = {}, + kind = 'k', + }: { uid?: string; namespace?: string; title?: string; kind?: string } = {}, ) { return { apiVersion: 'a', - kind: 'k', + kind, metadata: { name, ...(!!namespace && { namespace }), diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index ba165f96af..c328c83823 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -64,43 +64,47 @@ const defaultSortField: EntityOrder = { const DEFAULT_LIMIT = 20; -function parsePagination(input?: EntityPagination): { - limit?: number; - offset?: number; -} { +function parsePagination(input?: EntityPagination): EntityPagination { if (!input) { return {}; } let { limit, offset } = input; - if (input.after !== undefined) { - let cursor; - try { - const json = Buffer.from(input.after, 'base64').toString('utf8'); - cursor = JSON.parse(json); - } catch { - throw new InputError('Malformed after cursor, could not be parsed'); + if (input.after === undefined) { + return { limit, offset }; + } + + let cursor; + try { + const json = Buffer.from(input.after, 'base64').toString('utf8'); + cursor = JSON.parse(json); + } catch { + throw new InputError('Malformed after cursor, could not be parsed'); + } + + if (cursor.limit !== undefined) { + if (!Number.isInteger(cursor.limit)) { + throw new InputError('Malformed after cursor, limit was not an number'); } - if (cursor.limit !== undefined) { - if (!Number.isInteger(cursor.limit)) { - throw new InputError('Malformed after cursor, limit was not an number'); - } - limit = cursor.limit; - } - if (cursor.offset !== undefined) { - if (!Number.isInteger(cursor.offset)) { - throw new InputError('Malformed after cursor, offset was not a number'); - } - offset = cursor.offset; + limit = cursor.limit; + } + + if (cursor.offset !== undefined) { + if (!Number.isInteger(cursor.offset)) { + throw new InputError('Malformed after cursor, offset was not a number'); } + offset = cursor.offset; } return { limit, offset }; } -function stringifyPagination(input: { limit: number; offset: number }) { - const json = JSON.stringify({ limit: input.limit, offset: input.offset }); +function stringifyPagination( + input: Required>, +): string { + const { limit, offset } = input; + const json = JSON.stringify({ limit, offset }); const base64 = Buffer.from(json, 'utf8').toString('base64'); return base64; } @@ -111,24 +115,21 @@ function addCondition( filter: EntitiesSearchFilter, negate: boolean = false, entityIdField = 'entity_id', -) { +): void { + const key = filter.key.toLowerCase(); + const values = filter.values?.map(v => v.toLowerCase()); + // NOTE(freben): This used to be a set of OUTER JOIN, which may seem to // make a lot of sense. However, it had abysmal performance on sqlite // when datasets grew large, so we're using IN instead. const matchQuery = db('search') .select('search.entity_id') - .where({ key: filter.key.toLowerCase() }) + .where({ key }) .andWhere(function keyFilter() { - if (filter.values) { - if (filter.values.length === 1) { - this.where({ value: filter.values[0].toLowerCase() }); - } else { - this.andWhere( - 'value', - 'in', - filter.values.map(v => v.toLowerCase()), - ); - } + if (values?.length === 1) { + this.where({ value: values.at(0) }); + } else if (values) { + this.andWhere('value', 'in', values); } }); queryBuilder.andWhere(entityIdField, negate ? 'not in' : 'in', matchQuery); @@ -159,16 +160,16 @@ function parseFilter( negate: boolean = false, entityIdField = 'entity_id', ): Knex.QueryBuilder { + if (isNegationEntityFilter(filter)) { + return parseFilter(filter.not, query, db, !negate, entityIdField); + } + if (isEntitiesSearchFilter(filter)) { return query.andWhere(function filterFunction() { addCondition(this, db, filter, negate, entityIdField); }); } - if (isNegationEntityFilter(filter)) { - return parseFilter(filter.not, query, db, !negate, entityIdField); - } - return query[negate ? 'andWhereNot' : 'andWhere'](function filterFunction() { if (isOrEntityFilter(filter)) { for (const subFilter of filter.anyOf ?? []) { @@ -413,17 +414,18 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { const isOrderingDescending = sortField.order === 'desc'; if (prevItemOrderFieldValue) { - dbQuery.andWhere( - 'value', - isFetchingBackwards !== isOrderingDescending ? '<' : '>', - prevItemOrderFieldValue, - ); - dbQuery.orWhere(function nested() { - this.where('value', '=', prevItemOrderFieldValue).andWhere( - 'search.entity_id', + dbQuery.andWhere(function nested() { + this.where( + 'value', isFetchingBackwards !== isOrderingDescending ? '<' : '>', - prevItemUid, - ); + prevItemOrderFieldValue, + ) + .orWhere('value', '=', prevItemOrderFieldValue) + .andWhere( + 'search.entity_id', + isFetchingBackwards !== isOrderingDescending ? '<' : '>', + prevItemUid, + ); }); } @@ -677,10 +679,9 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { for (const facet of request.facets) { const dbQuery = db('search') - .join('final_entities', 'search.entity_id', 'final_entities.entity_id') .where('search.key', facet.toLocaleLowerCase('en-US')) - .count('search.entity_id as count') - .select({ value: 'search.original_value' }) + .whereNotNull('search.original_value') + .select({ value: 'search.original_value', count: db.raw('count(*)') }) .groupBy('search.original_value'); if (request?.filter) { diff --git a/plugins/catalog-customized/CHANGELOG.md b/plugins/catalog-customized/CHANGELOG.md index 138edaee90..0f8d9dccb1 100644 --- a/plugins/catalog-customized/CHANGELOG.md +++ b/plugins/catalog-customized/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/plugin-catalog-customized +## 0.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.11.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + ## 0.0.10-next.1 ### Patch Changes diff --git a/plugins/catalog-customized/package.json b/plugins/catalog-customized/package.json index 808f7a40e4..f7a6b0b80f 100644 --- a/plugins/catalog-customized/package.json +++ b/plugins/catalog-customized/package.json @@ -1,7 +1,7 @@ { "name": "@internal/plugin-catalog-customized", "description": "The internal Backstage Customizable plugin for browsing the Backstage catalog", - "version": "0.0.10-next.1", + "version": "0.0.10-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index eabfdaff12..00c907899a 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-graph +## 0.2.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.30-next.1 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index b9ac5ea1c1..bd9508783d 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.30-next.1", + "version": "0.2.30-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx index 85d9d97c7f..242bed6311 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { render, waitFor, screen } from '@testing-library/react'; +import { render, waitFor, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { CurveFilter } from './CurveFilter'; @@ -33,7 +33,9 @@ describe('', () => { expect(screen.getByText('Step Before')).toBeInTheDocument(); - await userEvent.click(screen.getByTestId('select')); + await userEvent.click( + within(screen.getByTestId('select')).getByRole('button'), + ); await userEvent.click(screen.getByText('Monotone X')); await waitFor(() => { diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx index cb660b5a25..dc74218316 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { render, waitFor, screen } from '@testing-library/react'; +import { render, waitFor, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Direction } from '../EntityRelationsGraph'; @@ -37,7 +37,9 @@ describe('', () => { expect(screen.getByText('Right to left')).toBeInTheDocument(); - await userEvent.click(screen.getByTestId('select')); + await userEvent.click( + within(screen.getByTestId('select')).getByRole('button'), + ); await userEvent.click(screen.getByText('Top to bottom')); await waitFor(() => { diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 738a28f3af..38ae6fbed8 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-import +## 0.9.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.1-next.1 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.9.8-next.1 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 7e7113a16c..5fb8bb1b5b 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.9.8-next.1", + "version": "0.9.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index bd9d13ace5..79ade18d8f 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-react +## 1.6.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + ## 1.6.0-next.1 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 37190aa4c8..d5808553cb 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "1.6.0-next.1", + "version": "1.6.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx index 04d8df3d16..2513f8dca9 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx @@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model'; import { ApiProvider } from '@backstage/core-app-api'; import { alertApiRef } from '@backstage/core-plugin-api'; import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; -import { fireEvent, waitFor, screen } from '@testing-library/react'; +import { fireEvent, waitFor, screen, within } from '@testing-library/react'; import { capitalize } from 'lodash'; import { default as React } from 'react'; import { catalogApiRef } from '../../api'; @@ -87,7 +87,7 @@ describe('', () => { expect(screen.getByText('Kind')).toBeInTheDocument(); const input = screen.getByTestId('select'); - fireEvent.click(input); + fireEvent.mouseDown(within(input).getByRole('button')); await waitFor(() => screen.getByText('Domain')); @@ -115,7 +115,7 @@ describe('', () => { , ); const input = screen.getByTestId('select'); - fireEvent.click(input); + fireEvent.mouseDown(within(input).getByRole('button')); await waitFor(() => screen.getByText('Domain')); fireEvent.click(screen.getByText('Domain')); @@ -171,7 +171,7 @@ describe('', () => { ); const input = screen.getByTestId('select'); - fireEvent.click(input); + fireEvent.mouseDown(within(input).getByRole('button')); expect( screen.getByRole('option', { name: 'Component' }), @@ -196,7 +196,7 @@ describe('', () => { expect(screen.getByText('Frob')).toBeInTheDocument(); const input = screen.getByTestId('select'); - fireEvent.click(input); + fireEvent.mouseDown(within(input).getByRole('button')); expect(screen.getByRole('option', { name: 'Domain' })).toBeInTheDocument(); }); }); diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index d61a6c480f..3e69c35180 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { fireEvent, waitFor, screen } from '@testing-library/react'; +import { fireEvent, waitFor, screen, within } from '@testing-library/react'; import { Entity } from '@backstage/catalog-model'; import { EntityTypePicker } from './EntityTypePicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; @@ -95,7 +95,7 @@ describe('', () => { expect(screen.getByText('Type')).toBeInTheDocument(); const input = screen.getByTestId('select'); - fireEvent.click(input); + fireEvent.mouseDown(within(input).getByRole('button')); await waitFor(() => screen.getByText('service')); @@ -119,7 +119,7 @@ describe('', () => { , ); const input = screen.getByTestId('select'); - fireEvent.click(input); + fireEvent.mouseDown(within(input).getByRole('button')); await waitFor(() => screen.getByText('service')); fireEvent.click(screen.getByText('service')); @@ -128,7 +128,7 @@ describe('', () => { type: new EntityTypeFilter(['service']), }); - fireEvent.click(input); + fireEvent.mouseDown(within(input).getByRole('button')); fireEvent.click(screen.getByText('all')); expect(updateFilters).toHaveBeenLastCalledWith({ type: undefined }); diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 1082396da1..8300e81422 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog +## 1.11.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 1.11.0-next.1 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 0d5a9badcd..2a0cfb4625 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "1.11.0-next.1", + "version": "1.11.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md index d895633ad4..93b77f90f8 100644 --- a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md +++ b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-cicd-statistics-module-gitlab +## 0.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-cicd-statistics@0.1.20-next.2 + ## 0.1.14-next.1 ### Patch Changes diff --git a/plugins/cicd-statistics-module-gitlab/package.json b/plugins/cicd-statistics-module-gitlab/package.json index 20d82e485f..37bf35a907 100644 --- a/plugins/cicd-statistics-module-gitlab/package.json +++ b/plugins/cicd-statistics-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics-module-gitlab", "description": "CI/CD Statistics plugin module; Gitlab CICD", - "version": "0.1.14-next.1", + "version": "0.1.14-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md index 3a802df672..a321674f8d 100644 --- a/plugins/cicd-statistics/CHANGELOG.md +++ b/plugins/cicd-statistics/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-cicd-statistics +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index a6a01ae05c..dce541c0b8 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics", "description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)", - "version": "0.1.20-next.1", + "version": "0.1.20-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 8afa59660d..afa6a83d48 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-circleci +## 0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.18-next.1 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 683b43795c..4108700972 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-circleci", "description": "A Backstage plugin that integrates towards Circle CI", - "version": "0.3.18-next.1", + "version": "0.3.18-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index ba462e356c..048c6b73ca 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-cloudbuild +## 0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.18-next.1 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 26a72a6c84..ef57f77fde 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cloudbuild", "description": "A Backstage plugin that integrates towards Google Cloud Build", - "version": "0.3.18-next.1", + "version": "0.3.18-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md index 405e65e814..c31c3ca3f3 100644 --- a/plugins/code-climate/CHANGELOG.md +++ b/plugins/code-climate/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-code-climate +## 0.1.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.18-next.1 ### Patch Changes diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json index 2edda59078..f650739510 100644 --- a/plugins/code-climate/package.json +++ b/plugins/code-climate/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-climate", - "version": "0.1.18-next.1", + "version": "0.1.18-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index 7f5789d3a5..e1ac2d6b77 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-code-coverage +## 0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.11-next.1 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 412c7d3281..7e40175240 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.2.11-next.1", + "version": "0.2.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/codescene/CHANGELOG.md b/plugins/codescene/CHANGELOG.md index 3c6338dfb0..4483bc8351 100644 --- a/plugins/codescene/CHANGELOG.md +++ b/plugins/codescene/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-codescene +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.13-next.0 ### Patch Changes diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json index 744634130c..05f2d8cde1 100644 --- a/plugins/codescene/package.json +++ b/plugins/codescene/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-codescene", - "version": "0.1.13-next.0", + "version": "0.1.13-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index e744ff73b1..71c47d650c 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-config-schema +## 0.1.41-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.41-next.0 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 893048274e..bf8bfc2d9d 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-config-schema", "description": "A Backstage plugin that lets you browse the configuration schema of your app", - "version": "0.1.41-next.0", + "version": "0.1.41-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 89ba2ebaa0..b9a289e963 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-cost-insights +## 0.12.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.12.7-next.1 ### Patch Changes diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md index 1e3f4e9de6..4fbf8a0cbe 100644 --- a/plugins/cost-insights/contrib/aws-cost-explorer-api.md +++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md @@ -75,7 +75,7 @@ try { We can use the data provided by the Cost Explorer API to implement CostInsightsApi methods such as `getGroupDailyCost`, `getProjectDailyCost`, and `getProductInsights`. You'll still need to provide external data for methods such as `getUserGroups` and `getAlerts`. -### 1. [getGroupDailyCost](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L93) +### 1. [getGroupDailyCost](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/cost-insights/src/api/CostInsightsApi.ts#L119) The `getGroupDailyCost` method is expected to return daily cost aggregations for a given group and interval time frame as a [`Cost`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights-common/src/types/Cost.ts). @@ -206,13 +206,13 @@ Sample `groupedCosts` based on the response: } ``` -### 2. [getProjectDailyCost](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L111) +### 2. [getProjectDailyCost](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/cost-insights/src/api/CostInsightsApi.ts#L137) The `getProjectDailyCost` method is expected to return daily cost aggregations for a given billing entity and interval time frame as a [`Cost`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights-common/src/types/Cost.ts). This should be similar to the `getGroupDailyCost` method implementation, but with an updated `LINKED_ACCOUNT` filter to get narrower cost data for a lower-level linked account. -### 3. [getProductInsights](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L111) +### 3. [getProductInsights](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/cost-insights/src/api/CostInsightsApi.ts#L165) The `getProductInsights` method is expected to return cost aggregations for a particular cloud product and interval time frame as an [`Entity`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights-common/src/types/Entity.ts). diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 4855aae01a..62295bf737 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cost-insights", "description": "A Backstage plugin that helps you keep track of your cloud spend", - "version": "0.12.7-next.1", + "version": "0.12.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/devtools-backend/.eslintrc.js b/plugins/devtools-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/devtools-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md new file mode 100644 index 0000000000..6fefbd1524 --- /dev/null +++ b/plugins/devtools-backend/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-devtools-backend + +## 0.1.0-next.0 + +### Minor Changes + +- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-devtools-common@0.1.0-next.0 + - @backstage/config@1.0.7 diff --git a/plugins/devtools-backend/README.md b/plugins/devtools-backend/README.md new file mode 100644 index 0000000000..e505a04963 --- /dev/null +++ b/plugins/devtools-backend/README.md @@ -0,0 +1,56 @@ +# DevTools Backend + +Welcome to the DevTools backend plugin! This plugin provides data for the [DevTools frontend](../devtools/) features. + +## Setup + +Here's how to get the DevTools Backend up and running: + +1. First we need to add the `@backstage/plugin-devtools-backend` package to your backend: + + ```sh + # From the Backstage root directory + cd packages/backend + yarn add @backstage/plugin-devtools-backend + ``` + +2. Then we will create a new file named `packages/backend/src/plugins/devtools.ts`, and add the + following to it: + + ```ts + import { createRouter } from '@backstage/plugin-devtools-backend'; + import { Router } from 'express'; + import type { PluginEnvironment } from '../types'; + + export default function createPlugin( + env: PluginEnvironment, + ): Promise { + return createRouter({ + logger: env.logger, + config: env.config, + permissions: env.permissions, + }); + } + ``` + +3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`: + + ```ts + import devTools from './plugins/devtools'; + // ... + async function main() { + // ... + // Add this line under the other lines that follow the useHotMemoize pattern + const devToolsEnv = useHotMemoize(module, () => createEnv('devtools')); + // ... + // Insert this line under the other lines that add their routers to apiRouter in the same way + apiRouter.use('/devtools', await devTools(devToolsEnv)); + ``` + +4. Now run `yarn start-backend` from the repo root +5. Finally open `http://localhost:7007/api/devtools/health` in a browser and it should return `{"status":"ok"}` + +## Links + +- [Frontend part of the plugin](../devtools/README.md) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/devtools-backend/api-report.md b/plugins/devtools-backend/api-report.md new file mode 100644 index 0000000000..e28f0697ab --- /dev/null +++ b/plugins/devtools-backend/api-report.md @@ -0,0 +1,39 @@ +## API Report File for "@backstage/plugin-devtools-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; +import { ConfigInfo } from '@backstage/plugin-devtools-common'; +import { DevToolsInfo } from '@backstage/plugin-devtools-common'; +import express from 'express'; +import { ExternalDependency } from '@backstage/plugin-devtools-common'; +import { Logger } from 'winston'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export class DevToolsBackendApi { + constructor(logger: Logger, config: Config); + // (undocumented) + listConfig(): Promise; + // (undocumented) + listExternalDependencyDetails(): Promise; + // (undocumented) + listInfo(): Promise; +} + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + config: Config; + // (undocumented) + devToolsBackendApi?: DevToolsBackendApi; + // (undocumented) + logger: Logger; + // (undocumented) + permissions: PermissionEvaluator; +} +``` diff --git a/plugins/devtools-backend/config.d.ts b/plugins/devtools-backend/config.d.ts new file mode 100644 index 0000000000..15e70afcf8 --- /dev/null +++ b/plugins/devtools-backend/config.d.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Config { + /** + * DevTools configuration. + */ + devTools?: { + /** + * External dependency configuration. + */ + externalDependencies?: { + /** + * The list of endpoints to check. + */ + endpoints?: Array<{ + /** + * The name of the endpoint. + */ + name: string; + /** + * Type of check to perform; currently fetch or ping + */ + type: string; + /** + * The target of the endpoint; currently either a URL for fetch or server name for ping. + */ + target: string; + }>; + }; + }; +} diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json new file mode 100644 index 0000000000..7235f1293a --- /dev/null +++ b/plugins/devtools-backend/package.json @@ -0,0 +1,63 @@ +{ + "name": "@backstage/plugin-devtools-backend", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/cli-common": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/config-loader": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-devtools-common": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-permission-node": "workspace:^", + "@backstage/types": "workspace:^", + "@manypkg/get-packages": "^1.1.3", + "@types/express": "*", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "^3.0.0-rc.4", + "express": "^4.18.1", + "express-promise-router": "^4.1.0", + "fs-extra": "^10.0.0", + "lodash": "^4.17.21", + "node-fetch": "^2.6.7", + "ping": "^0.4.1", + "semver": "^7.3.2", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/minimist": "^1.2.0", + "@types/ping": "^0.4.1", + "@types/supertest": "^2.0.8", + "@types/yarnpkg__lockfile": "^1.1.4", + "msw": "^0.47.0", + "supertest": "^6.2.4" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/devtools-backend/src/api/DevToolsBackendApi.ts b/plugins/devtools-backend/src/api/DevToolsBackendApi.ts new file mode 100644 index 0000000000..efd0407b31 --- /dev/null +++ b/plugins/devtools-backend/src/api/DevToolsBackendApi.ts @@ -0,0 +1,254 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config, ConfigReader } from '@backstage/config'; +import { loadConfigSchema } from '@backstage/config-loader'; +import { + PackageDependency, + DevToolsInfo, + ExternalDependency, + Endpoint, + ExternalDependencyStatus, + ConfigInfo, +} from '@backstage/plugin-devtools-common'; + +import { JsonObject } from '@backstage/types'; +import { Logger } from 'winston'; +import fetch from 'node-fetch'; +import { findPaths } from '@backstage/cli-common'; +import { getPackages } from '@manypkg/get-packages'; +import ping from 'ping'; +import os from 'os'; +import fs from 'fs-extra'; +import { Lockfile } from '../util/Lockfile'; +import { memoize } from 'lodash'; +import { assertError } from '@backstage/errors'; + +/** @public */ +export class DevToolsBackendApi { + public constructor( + private readonly logger: Logger, + private readonly config: Config, + ) {} + + public async listExternalDependencyDetails(): Promise { + const result: ExternalDependency[] = []; + + const endpoints = this.config.getOptional( + 'devTools.externalDependencies.endpoints', + ); + if (!endpoints) { + // No external dependency endpoints configured + return result; + } + for (const endpoint of endpoints) { + this.logger?.info( + `Checking external dependency "${endpoint.name}" at "${endpoint.target}"`, + ); + + switch (endpoint.type) { + case 'ping': { + const pingResult = await this.pingExternalDependency(endpoint); + result.push(pingResult); + break; + } + case 'fetch': { + const fetchResult = await this.fetchExternalDependency(endpoint); + result.push(fetchResult); + break; + } + default: + return result; + } + } + + return result; + } + + private async fetchExternalDependency( + endpoint: Endpoint, + ): Promise { + let status; + let error; + + await fetch(endpoint.target) + .then(res => { + status = + res.status === 200 + ? ExternalDependencyStatus.healthy + : ExternalDependencyStatus.unhealthy; + this.logger.debug( + `Fetch for ${endpoint.name} resulted in status code "${res.status}"`, + ); + }) + .catch((err: Error) => { + this.logger.error(`Fetch failed for ${endpoint.name} - ${err.message}`); + error = err.message; + }); + + const result: ExternalDependency = { + name: endpoint.name, + type: endpoint.type, + target: endpoint.target, + status: status ?? ExternalDependencyStatus.unhealthy, + error: error ?? undefined, + }; + + return result; + } + + private async pingExternalDependency( + endpoint: Endpoint, + ): Promise { + const pingResult = await ping.promise.probe(endpoint.target); + + let error; + if ( + pingResult.packetLoss === '100.000' || + pingResult.packetLoss === 'unknown' + ) { + this.logger.error( + `Ping failed for ${endpoint.name} - ${pingResult.output}`, + ); + error = + pingResult.output === '' + ? `${endpoint.target} - Unknown` + : pingResult.output; + } + + this.logger.debug( + `Ping results for ${endpoint.name}: ${pingResult.output}`, + ); + + const result: ExternalDependency = { + name: endpoint.name, + type: endpoint.type, + target: endpoint.target, + status: pingResult.alive + ? ExternalDependencyStatus.healthy + : ExternalDependencyStatus.unhealthy, + error: error ?? undefined, + }; + + return result; + } + + public async listConfig(): Promise { + /* eslint-disable-next-line no-restricted-syntax */ + const paths = findPaths(__dirname); + + const { packages } = await getPackages(paths.targetDir); + const schemaFunc = async () => { + return await loadConfigSchema({ + dependencies: packages.map(p => p.packageJson.name), + }); + }; + + const schemaMemo = memoize(schemaFunc); + const schema = await schemaMemo(); + + const configInfo: ConfigInfo = { + config: undefined, + error: undefined, + }; + try { + const config = { + data: this.config.get() as JsonObject, + context: 'inline', + }; + const sanitizedConfigs = schema.process([config], { + ignoreSchemaErrors: false, + valueTransform: (value, context) => + context.visibility === 'secret' ? '' : value, + }); + + const data = ConfigReader.fromConfigs(sanitizedConfigs).get(); + configInfo.config = data; + } catch (error) { + assertError(error); + // The config is not valid for some reason but we want to be able to see it still + const config = { + data: this.config.get() as JsonObject, + context: 'inline', + }; + const sanitizedConfigs = schema.process([config], { + ignoreSchemaErrors: true, + valueTransform: (value, context) => + context.visibility === 'secret' ? '' : value, + }); + + const data = ConfigReader.fromConfigs(sanitizedConfigs).get(); + configInfo.config = data; + configInfo.error = { + name: error.name, + message: error.message, + messages: error.messages as string[] | undefined, + stack: error.stack, + }; + } + + return configInfo; + } + + public async listInfo(): Promise { + const operatingSystem = `${os.type} ${os.release} - ${os.platform}/${os.arch}`; + const nodeJsVersion = process.version; + + /* eslint-disable-next-line no-restricted-syntax */ + const paths = findPaths(__dirname); + const backstageFile = paths.resolveTargetRoot('backstage.json'); + let backstageJson = undefined; + if (fs.existsSync(backstageFile)) { + const buffer = await fs.readFile(backstageFile); + backstageJson = JSON.parse(buffer.toString()); + } + + const lockfilePath = paths.resolveTargetRoot('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + + const deps = [...lockfile.keys()].filter(n => n.startsWith('@backstage/')); + + const infoDependencies: PackageDependency[] = []; + for (const dep of deps) { + const versions = new Set(lockfile.get(dep)!.map(i => i.version)); + const infoDependency: PackageDependency = { + name: dep, + versions: [...versions].join(', '), + }; + infoDependencies.push(infoDependency); + } + + const info: DevToolsInfo = { + operatingSystem: operatingSystem ?? 'N/A', + nodeJsVersion: nodeJsVersion ?? 'N/A', + backstageVersion: + backstageJson && backstageJson.version ? backstageJson.version : 'N/A', + dependencies: infoDependencies, + }; + + return info; + } +} + +export function isValidUrl(url: string): boolean { + try { + // eslint-disable-next-line no-new + new URL(url); + return true; + } catch { + return false; + } +} diff --git a/plugins/devtools-backend/src/api/index.ts b/plugins/devtools-backend/src/api/index.ts new file mode 100644 index 0000000000..ed3390b113 --- /dev/null +++ b/plugins/devtools-backend/src/api/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { DevToolsBackendApi } from './DevToolsBackendApi'; diff --git a/plugins/devtools-backend/src/index.ts b/plugins/devtools-backend/src/index.ts new file mode 100644 index 0000000000..e1545e0022 --- /dev/null +++ b/plugins/devtools-backend/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Backstage DevTools backend plugin that contains the API for retrieving package dependencies and current configuration which is used by the Backstage DevTools frontend plugin. + * + * @packageDocumentation + */ + +export { DevToolsBackendApi } from './api'; +export * from './service/router'; diff --git a/plugins/devtools-backend/src/run.ts b/plugins/devtools-backend/src/run.ts new file mode 100644 index 0000000000..d945aa13f0 --- /dev/null +++ b/plugins/devtools-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/devtools-backend/src/service/router.test.ts b/plugins/devtools-backend/src/service/router.test.ts new file mode 100644 index 0000000000..f3c78242cf --- /dev/null +++ b/plugins/devtools-backend/src/service/router.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import express from 'express'; +import request from 'supertest'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { createRouter } from './router'; + +const mockedAuthorize: jest.MockedFunction = + jest.fn(); +const mockedPermissionQuery: jest.MockedFunction< + PermissionEvaluator['authorizeConditional'] +> = jest.fn(); + +const permissionEvaluator: PermissionEvaluator = { + authorize: mockedAuthorize, + authorizeConditional: mockedPermissionQuery, +}; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + config: new ConfigReader({ + healthCheck: { + endpoint: [ + { + name: '', + type: '', + target: '', + }, + ], + }, + }), + permissions: permissionEvaluator, + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/plugins/devtools-backend/src/service/router.ts b/plugins/devtools-backend/src/service/router.ts new file mode 100644 index 0000000000..a346ad3183 --- /dev/null +++ b/plugins/devtools-backend/src/service/router.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + AuthorizeResult, + PermissionEvaluator, +} from '@backstage/plugin-permission-common'; +import { + devToolsConfigReadPermission, + devToolsExternalDependenciesReadPermission, + devToolsInfoReadPermission, +} from '@backstage/plugin-devtools-common'; + +import { Config } from '@backstage/config'; +import { DevToolsBackendApi } from '../api'; +import { Logger } from 'winston'; +import { NotAllowedError } from '@backstage/errors'; +import Router from 'express-promise-router'; +import { errorHandler } from '@backstage/backend-common'; +import express from 'express'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; + +/** @public */ +export interface RouterOptions { + devToolsBackendApi?: DevToolsBackendApi; + logger: Logger; + config: Config; + permissions: PermissionEvaluator; +} + +/** @public */ +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger, config, permissions } = options; + + const devToolsBackendApi = + options.devToolsBackendApi || new DevToolsBackendApi(logger, config); + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_req, res) => { + res.status(200).json({ status: 'ok' }); + }); + + router.get('/info', async (req, response) => { + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + const decision = ( + await permissions.authorize( + [{ permission: devToolsInfoReadPermission }], + { + token, + }, + ) + )[0]; + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError('Unauthorized'); + } + + const info = await devToolsBackendApi.listInfo(); + + response.status(200).json(info); + }); + + router.get('/config', async (req, response) => { + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + const decision = ( + await permissions.authorize( + [{ permission: devToolsConfigReadPermission }], + { + token, + }, + ) + )[0]; + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError('Unauthorized'); + } + + const configList = await devToolsBackendApi.listConfig(); + + response.status(200).json(configList); + }); + + router.get('/external-dependencies', async (req, response) => { + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + const decision = ( + await permissions.authorize( + [{ permission: devToolsExternalDependenciesReadPermission }], + { + token, + }, + ) + )[0]; + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError('Unauthorized'); + } + + const health = await devToolsBackendApi.listExternalDependencyDetails(); + + response.status(200).json(health); + }); + + router.use(errorHandler()); + return router; +} diff --git a/plugins/devtools-backend/src/service/standaloneServer.ts b/plugins/devtools-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..d2fdce169d --- /dev/null +++ b/plugins/devtools-backend/src/service/standaloneServer.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ServerTokenManager, + SingleHostDiscovery, + createServiceBuilder, + loadBackendConfig, +} from '@backstage/backend-common'; + +import { Logger } from 'winston'; +import { Server } from 'http'; +import { ServerPermissionClient } from '@backstage/plugin-permission-node'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'devtools-backend-backend' }); + const config = await loadBackendConfig({ logger, argv: process.argv }); + const discovery = SingleHostDiscovery.fromConfig(config); + const tokenManager = ServerTokenManager.fromConfig(config, { + logger, + }); + const permissions = ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager, + }); + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + config, + permissions, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/devtools-backend', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/devtools-backend/src/setupTests.ts b/plugins/devtools-backend/src/setupTests.ts new file mode 100644 index 0000000000..813cdeaae3 --- /dev/null +++ b/plugins/devtools-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/devtools-backend/src/util/Lockfile.ts b/plugins/devtools-backend/src/util/Lockfile.ts new file mode 100644 index 0000000000..52dbb847bc --- /dev/null +++ b/plugins/devtools-backend/src/util/Lockfile.ts @@ -0,0 +1,317 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import semver from 'semver'; +import { parseSyml, stringifySyml } from '@yarnpkg/parsers'; +import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; + +const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; + +type LockfileData = { + [entry: string]: { + version: string; + resolved?: string; + integrity?: string; + dependencies?: { [name: string]: string }; + }; +}; + +type LockfileQueryEntry = { + range: string; + version: string; +}; + +/** Entries that have an invalid version range, for example an npm tag */ +type AnalyzeResultInvalidRange = { + name: string; + range: string; +}; + +/** Entries that can be deduplicated by bumping to an existing higher version */ +type AnalyzeResultNewVersion = { + name: string; + range: string; + oldVersion: string; + newVersion: string; +}; + +/** Entries that would need a dependency update in package.json to be deduplicated */ +type AnalyzeResultNewRange = { + name: string; + oldRange: string; + newRange: string; + oldVersion: string; + newVersion: string; +}; + +type AnalyzeResult = { + invalidRanges: AnalyzeResultInvalidRange[]; + newVersions: AnalyzeResultNewVersion[]; + newRanges: AnalyzeResultNewRange[]; +}; + +function parseLockfile(lockfileContents: string) { + try { + return { + object: parseSyml(lockfileContents), + type: 'success', + }; + } catch (err) { + return { + object: null, + type: err, + }; + } +} + +// the new yarn header is handled out of band of the parsing +// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746 +const NEW_HEADER = `${[ + `# This file is generated by running "yarn install" inside your project.\n`, + `# Manual changes might be lost - proceed with caution!\n`, +].join(``)}\n`; + +function stringifyLockfile(data: LockfileData, legacy: boolean) { + return legacy + ? legacyStringifyLockfile(data) + : NEW_HEADER + stringifySyml(data); +} +// taken from yarn parser package +// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136 +const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i; + +// these are special top level yarn keys. +// https://github.com/yarnpkg/berry/blob/9bd61fbffb83d0b8166a9cc26bec3a58743aa453/packages/yarnpkg-parsers/sources/syml.ts#L9 +const SPECIAL_OBJECT_KEYS = [ + `__metadata`, + `version`, + `resolution`, + `dependencies`, + `peerDependencies`, + `dependenciesMeta`, + `peerDependenciesMeta`, + `binaries`, +]; + +export class Lockfile { + static async load(path: string) { + const lockfileContents = await fs.readFile(path, 'utf8'); + const legacy = LEGACY_REGEX.test(lockfileContents); + const lockfile = parseLockfile(lockfileContents); + if (lockfile.type !== 'success') { + throw new Error(`Failed yarn.lock parse with ${lockfile.type}`); + } + + const data = lockfile.object as LockfileData; + const packages = new Map(); + + for (const [key, value] of Object.entries(data)) { + if (SPECIAL_OBJECT_KEYS.includes(key)) continue; + + const [, name, range] = ENTRY_PATTERN.exec(key) ?? []; + if (!name) { + throw new Error(`Failed to parse yarn.lock entry '${key}'`); + } + + let queries = packages.get(name); + if (!queries) { + queries = []; + packages.set(name, queries); + } + queries.push({ range, version: value.version }); + } + + return new Lockfile(path, packages, data, legacy); + } + + private constructor( + private readonly path: string, + private readonly packages: Map, + private readonly data: LockfileData, + private readonly legacy: boolean = false, + ) {} + + /** Get the entries for a single package in the lockfile */ + get(name: string): LockfileQueryEntry[] | undefined { + return this.packages.get(name); + } + + /** Returns the name of all packages available in the lockfile */ + keys(): IterableIterator { + return this.packages.keys(); + } + + /** Analyzes the lockfile to identify possible actions and warnings for the entries */ + analyze(options?: { filter?: (name: string) => boolean }): AnalyzeResult { + const { filter } = options ?? {}; + const result: AnalyzeResult = { + invalidRanges: [], + newVersions: [], + newRanges: [], + }; + + for (const [name, allEntries] of this.packages) { + if (filter && !filter(name)) { + continue; + } + + // Get rid of and signal any invalid ranges upfront + const invalid = allEntries.filter(e => !semver.validRange(e.range)); + result.invalidRanges.push( + ...invalid.map(({ range }) => ({ name, range })), + ); + + // Grab all valid entries, if there aren't at least 2 different valid ones we're done + const entries = allEntries.filter(e => semver.validRange(e.range)); + if (entries.length < 2) { + continue; + } + + // Find all versions currently in use + const versions = Array.from(new Set(entries.map(e => e.version))).sort( + (v1, v2) => semver.rcompare(v1, v2), + ); + + // If we're not using at least 2 different versions we're done + if (versions.length < 2) { + continue; + } + + const acceptedVersions = new Set(); + for (const { version, range } of entries) { + // Finds the highest matching version from the the known versions + // TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one + const acceptedVersion = versions.find(v => semver.satisfies(v, range)); + if (!acceptedVersion) { + throw new Error( + `No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`, + ); + } + + if (acceptedVersion !== version) { + result.newVersions.push({ + name, + range, + newVersion: acceptedVersion, + oldVersion: version, + }); + } + + acceptedVersions.add(acceptedVersion); + } + + // If all ranges were able to accept the same version, we're done + if (acceptedVersions.size === 1) { + continue; + } + + // Find the max version that we may want bump older packages to + const maxVersion = Array.from(acceptedVersions).sort(semver.rcompare)[0]; + // Find all existing ranges that satisfy the new max version, and pick the one that + // results in the highest minimum allowed version, usually being the more specific one + const maxEntry = entries + .filter(e => semver.satisfies(maxVersion, e.range)) + .map(e => ({ e, min: semver.minVersion(e.range) })) + .filter(p => p.min) + .sort((a, b) => semver.rcompare(a.min!, b.min!))[0]?.e; + if (!maxEntry) { + throw new Error( + `No entry found that satisfies max version '${maxVersion}'`, + ); + } + + // Find all entries that don't satisfy the max version + for (const { version, range } of entries) { + if (semver.satisfies(maxVersion, range)) { + continue; + } + + result.newRanges.push({ + name, + oldRange: range, + newRange: maxEntry.range, + oldVersion: version, + newVersion: maxVersion, + }); + } + } + + return result; + } + + remove(name: string, range: string): boolean { + const query = `${name}@${range}`; + const existed = Boolean(this.data[query]); + delete this.data[query]; + + const newEntries = this.packages.get(name)?.filter(e => e.range !== range); + if (newEntries) { + this.packages.set(name, newEntries); + } + + return existed; + } + + /** Modifies the lockfile by bumping packages to the suggested versions */ + replaceVersions(results: AnalyzeResultNewVersion[]) { + for (const { name, range, oldVersion, newVersion } of results) { + const query = `${name}@${range}`; + + // Update the backing data + const entryData = this.data[query]; + if (!entryData) { + throw new Error(`No entry data for ${query}`); + } + if (entryData.version !== oldVersion) { + throw new Error( + `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`, + ); + } + + // Modifying the data in the entry is not enough, we need to reference an existing version object + const matchingEntry = Object.entries(this.data).find( + ([q, e]) => q.startsWith(`${name}@`) && e.version === newVersion, + ); + if (!matchingEntry) { + throw new Error( + `No matching entry found for ${name} at version ${newVersion}`, + ); + } + this.data[query] = matchingEntry[1]; + + // Update our internal data structure + const entry = this.packages.get(name)?.find(e => e.range === range); + if (!entry) { + throw new Error(`No entry data for ${query}`); + } + if (entry.version !== oldVersion) { + throw new Error( + `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`, + ); + } + entry.version = newVersion; + } + } + + async save() { + await fs.writeFile(this.path, this.toString(), 'utf8'); + } + + toString() { + return stringifyLockfile(this.data, this.legacy); + } +} diff --git a/plugins/devtools-common/.eslintrc.js b/plugins/devtools-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/devtools-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/devtools-common/CHANGELOG.md b/plugins/devtools-common/CHANGELOG.md new file mode 100644 index 0000000000..79c40a24c1 --- /dev/null +++ b/plugins/devtools-common/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/plugin-devtools-common + +## 0.1.0-next.0 + +### Minor Changes + +- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details! diff --git a/plugins/devtools-common/README.md b/plugins/devtools-common/README.md new file mode 100644 index 0000000000..5be5e58773 --- /dev/null +++ b/plugins/devtools-common/README.md @@ -0,0 +1,3 @@ +# DevTools Common + +Common types and permissions for the DevTools plugin. diff --git a/plugins/devtools-common/api-report.md b/plugins/devtools-common/api-report.md new file mode 100644 index 0000000000..7e38ff05b0 --- /dev/null +++ b/plugins/devtools-common/api-report.md @@ -0,0 +1,72 @@ +## API Report File for "@backstage/plugin-devtools-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BasicPermission } from '@backstage/plugin-permission-common'; +import { JsonValue } from '@backstage/types'; + +// @public (undocumented) +export type ConfigError = { + name: string; + message: string; + messages?: string[]; + stack?: string; +}; + +// @public (undocumented) +export type ConfigInfo = { + config?: JsonValue; + error?: ConfigError; +}; + +// @public (undocumented) +export const devToolsAdministerPermission: BasicPermission; + +// @public (undocumented) +export const devToolsConfigReadPermission: BasicPermission; + +// @public (undocumented) +export const devToolsExternalDependenciesReadPermission: BasicPermission; + +// @public (undocumented) +export type DevToolsInfo = { + operatingSystem: string; + nodeJsVersion: string; + backstageVersion: string; + dependencies: PackageDependency[]; +}; + +// @public (undocumented) +export const devToolsInfoReadPermission: BasicPermission; + +// @public (undocumented) +export type Endpoint = { + name: string; + type: string; + target: string; +}; + +// @public (undocumented) +export type ExternalDependency = { + name: string; + type: string; + target: string; + status: string; + error?: string; +}; + +// @public (undocumented) +export enum ExternalDependencyStatus { + // (undocumented) + healthy = 'Healthy', + // (undocumented) + unhealthy = 'Unhealthy', +} + +// @public (undocumented) +export type PackageDependency = { + name: string; + versions: string; +}; +``` diff --git a/plugins/devtools-common/package.json b/plugins/devtools-common/package.json new file mode 100644 index 0000000000..ba48da49ad --- /dev/null +++ b/plugins/devtools-common/package.json @@ -0,0 +1,35 @@ +{ + "name": "@backstage/plugin-devtools-common", + "description": "Common functionalities for the devtools plugin", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/plugin-permission-common": "workspace:^", + "@backstage/types": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/devtools-common/src/index.ts b/plugins/devtools-common/src/index.ts new file mode 100644 index 0000000000..be7426394b --- /dev/null +++ b/plugins/devtools-common/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common types and permissions for the DevTools frontend and backend plugins. + * + * @packageDocumentation + */ + +export * from './types'; +export * from './permissions'; diff --git a/plugins/devtools-common/src/permissions.ts b/plugins/devtools-common/src/permissions.ts new file mode 100644 index 0000000000..ae62cfdc19 --- /dev/null +++ b/plugins/devtools-common/src/permissions.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createPermission } from '@backstage/plugin-permission-common'; + +/** + * @public + */ +export const devToolsAdministerPermission = createPermission({ + name: 'devtools.administer', + attributes: { action: 'read' }, +}); + +/** + * @public + */ +export const devToolsInfoReadPermission = createPermission({ + name: 'devtools.info', + attributes: { action: 'read' }, +}); + +/** + * @public + */ +export const devToolsConfigReadPermission = createPermission({ + name: 'devtools.config', + attributes: { action: 'read' }, +}); + +/** + * @public + */ +export const devToolsExternalDependenciesReadPermission = createPermission({ + name: 'devtools.external-dependencies', + attributes: { action: 'read' }, +}); diff --git a/plugins/devtools-common/src/setupTests.ts b/plugins/devtools-common/src/setupTests.ts new file mode 100644 index 0000000000..813cdeaae3 --- /dev/null +++ b/plugins/devtools-common/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/devtools-common/src/types.ts b/plugins/devtools-common/src/types.ts new file mode 100644 index 0000000000..d08232b155 --- /dev/null +++ b/plugins/devtools-common/src/types.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JsonValue } from '@backstage/types'; + +/** @public */ +export type Endpoint = { + name: string; + type: string; + target: string; +}; + +/** @public */ +export type ExternalDependency = { + name: string; + type: string; + target: string; + status: string; + error?: string; +}; + +/** @public */ +export type DevToolsInfo = { + operatingSystem: string; + nodeJsVersion: string; + backstageVersion: string; + dependencies: PackageDependency[]; +}; + +/** @public */ +export type PackageDependency = { + name: string; + versions: string; +}; + +/** @public */ +export enum ExternalDependencyStatus { + healthy = 'Healthy', + unhealthy = 'Unhealthy', +} + +/** @public */ +export type ConfigInfo = { + config?: JsonValue; + error?: ConfigError; +}; + +/** @public */ +export type ConfigError = { + name: string; + message: string; + messages?: string[]; + stack?: string; +}; diff --git a/plugins/devtools/.eslintrc.js b/plugins/devtools/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/devtools/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md new file mode 100644 index 0000000000..0fe41704c6 --- /dev/null +++ b/plugins/devtools/CHANGELOG.md @@ -0,0 +1,16 @@ +# @backstage/plugin-devtools + +## 0.1.0-next.0 + +### Minor Changes + +- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details! + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-devtools-common@0.1.0-next.0 + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md new file mode 100644 index 0000000000..fc9f1f7312 --- /dev/null +++ b/plugins/devtools/README.md @@ -0,0 +1,375 @@ +# DevTools + +Welcome to the DevTools plugin! This plugin helps elevate useful information about a Backstage instance through the frontend, which can be helpful for an integrator for troubleshooting, reviewing, and understanding their installation. + +## Features + +The DevTools plugin comes with two tabs out of the box. + +### Info + +Lists helpful information about your current running Backstage instance such as: OS, NodeJS version, Backstage version, and package versions. + +![Example of Info tab](./docs/devtools-info-tab.png) + +### Config + +Lists the configuration being used by your current running Backstage instance. + +![Example of Config tab](./docs/devtools-config-tab.png) + +## Optional Features + +The DevTools plugin can be setup with other tabs with additional helpful features. + +### External Dependencies + +Lists the status of configured External Dependencies based on your current running Backstage instance's ability to reach them. + +![Example of external dependencies tab](./docs/devtools-external-dependencies.png) + +## Setup + +The following sections will help you get the DevTools plugin setup and running. + +### Backend + +You need to setup the [DevTools backend plugin](../devtools-backend/README.md) before you move forward with any of the following steps if you haven't already. + +### Frontend + +To setup the DevTools frontend you'll need to do the following steps: + +1. First we need to add the `@backstage/plugin-devtools` package to your frontend app: + + ```sh + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-devtools + ``` + +2. Now open the `packages/app/src/App.tsx` file +3. Then after all the import statements add the following line: + + ```ts + import { DevToolsPage } from '@backstage/plugin-devtools'; + ``` + +4. In this same file just before the closing ``, this will be near the bottom of the file, add this line: + + ```ts + } /> + ``` + +5. Next open the `packages/app/src/components/Root/Root.tsx` file +6. We want to add this icon import after all the existing import statements: + + ```ts + import BuildIcon from '@material-ui/icons/Build'; + ``` + +7. Then add this line just after the `` line: + + ```ts + + ``` + +8. Now run `yarn dev` from the root of your project and you should see the DevTools option show up just below Settings in your sidebar and clicking on it will get you to the [Info tab](#info) + +## Customizing + +The DevTools plugin has been designed so that you can customize the tabs to suite your needs. You may only want some or none of the out of the box tabs or you may want to add your own. The following sections explains how to do that (assuming you've already done the [setup steps](#setup)). As part of this example we'll also be showing how you can add the optional [External Dependencies](#external-dependencies) tab. + +1. In the `packages/app/src/components` folder create a new sub-folder called `devtools` +2. Then in this new `devtools` folder add a file called `CustomDevToolsPage.tsx` +3. In the `CustomDevToolsPage.tsx` file add the following content: + + ```tsx + import { + ConfigContent, + ExternalDependenciesContent, + InfoContent, + } from '@backstage/plugin-devtools'; + import { DevToolsLayout } from '@backstage/plugin-devtools'; + import React from 'react'; + + export const DevToolsPage = () => { + return ( + + + + + + + + + + + + ); + }; + + export const customDevToolsPage = ; + ``` + +4. Now open the `packages/app/src/App.tsx` file and add the following import after all the existing import statements: + + ```ts + import { customDevToolsPage } from './components/devtools/CustomDevToolsPage'; + ``` + +5. Then we need to adjust our route as follows + + ```diff + - } /> + + } > + + {customDevToolsPage} + + + ``` + +6. Now run `yarn dev` from the root of your project. When you go to the DevTools you'll now see you have a third tab for [External Dependencies](#external-dependencies) + +With this setup you can add or remove the tabs as you'd like or add your own simply by editing your `CustomDevToolsPage.tsx` file + +## Permissions + +The DevTools plugin supports the [permissions framework](https://backstage.io/docs/permissions/overview), the following sections outline how you can use them with the assumption that you have the permissions framework setup and working. + +**Note:** These sections are intended as guidance and are completely optional. The DevTools plugin will work with the permission framework off or on without any specific policy setup. + +### Secure Sidebar Option + +To use the permission framework to secure the DevTools sidebar option you'll want to do the following: + +1. First we need to add the `@backstage/plugin-devtools-common` package to your frontend app: + + ```sh + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-devtools + ``` + +2. Then open the `packages/app/src/components/Root/Root.tsx` file +3. The add these imports after all the existing import statements: + + ```ts + import { devToolsAdministerPermission } from '@backstage/plugin-devtools-common'; + import { RequirePermission } from '@backstage/plugin-permission-react'; + ``` + +4. Then make the following change: + + ```diff + - + + }> + + + + + ``` + +### Secure the DevTools Route + +To use the permission framework to secure the DevTools route you'll want to do the following: + +1. First we need to add the `@backstage/plugin-devtools-common` package to your frontend app (skip this step if you've already done this): + + ```sh + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-devtools-common + ``` + +2. Then open the `packages/app/src/App.tsx` file +3. The add this import after all the existing import statements: + + ```ts + import { devToolsAdministerPermission } from '@backstage/plugin-devtools-common'; + ``` + +4. Then make the following change: + + ```diff + - } /> + + + + + + + + } + + /> + ``` + +Note: if you are using a `customDevToolsPage` as per the [Customizing](#customizing) documentation the changes for Step 4 will be: + +```diff +- } /> ++ ++ ++ ++ } ++ > ++ {customDevToolsPage} ++ +``` + +### Permission Policy + +Here is an example permission policy that you might use to secure the DevTools plugin: + +```ts +// packages/backend/src/plugins/permission.ts + +class TestPermissionPolicy implements PermissionPolicy { + async handle(request: PolicyQuery): Promise { + if (isPermission(request.permission, devToolsAdministerPermission)) { + if ( + user?.identity.ownershipEntityRefs.includes( + 'group:default/backstage-admins', + ) + ) { + return { result: AuthorizeResult.ALLOW }; + } + return { result: AuthorizeResult.DENY }; + } + + if (isPermission(request.permission, devToolsInfoReadPermission)) { + if ( + user?.identity.ownershipEntityRefs.includes( + 'group:default/backstage-admins', + ) + ) { + return { result: AuthorizeResult.ALLOW }; + } + return { result: AuthorizeResult.DENY }; + } + + if (isPermission(request.permission, devToolsConfigReadPermission)) { + if ( + user?.identity.ownershipEntityRefs.includes( + 'group:default/backstage-admins', + ) + ) { + return { result: AuthorizeResult.ALLOW }; + } + return { result: AuthorizeResult.DENY }; + } + + if ( + isPermission( + request.permission, + devToolsExternalDependenciesReadPermission, + ) + ) { + if ( + user?.identity.ownershipEntityRefs.includes( + 'group:default/backstage-admins', + ) + ) { + return { result: AuthorizeResult.ALLOW }; + } + return { result: AuthorizeResult.DENY }; + } + + return { result: AuthorizeResult.ALLOW }; + } +} +``` + +To use this policy you'll need to make sure to add the `@backstage/plugin-devtools-common` package to your backend you can do that by running this command: + +```sh +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-devtools-common +``` + +You'll also need to add these imports: + +```ts +import { + devToolsAdministerPermission, + devToolsConfigReadPermission, + devToolsExternalDependenciesReadPermission, + devToolsInfoReadPermission, +} from '@backstage/plugin-devtools-common'; +``` + +**Note:** The group "group:default/backstage-admins" is simply an example and does not exist. You can point this to any group you have in your catalog instead. + +### Customizing with Permissions + +If you followed the [Customizing](#customizing) documentation and want to use permission there this is what your `CustomDevToolsPage.tsx` would look like: + +```tsx +import { + ConfigContent, + ExternalDependenciesContent, + InfoContent, +} from '@backstage/plugin-devtools'; +import { DevToolsLayout } from '@backstage/plugin-devtools'; +import { + devToolsConfigReadPermission, + devToolsExternalDependenciesReadPermission, + devToolsInfoReadPermission, +} from '@backstage/plugin-devtools-common'; +import { RequirePermission } from '@backstage/plugin-permission-react'; +import React from 'react'; + +const DevToolsPage = () => { + return ( + + + + + + + + + + + + + + + + + + ); +}; + +export const customDevToolsPage = ; +``` + +## Configuration + +The following sections outline the configuration for the DevTools plugin + +### External Dependencies Configuration + +If you decide to use the External Dependencies tab then you'll need to setup the configuration for it in your `app-config.yaml`, if there is no config setup then the tab will be empty. Here's an example: + +```yaml +devTools: + externalDependencies: + endpoints: + - name: 'Google' + type: 'fetch' + target: 'https://google.ca' + - name: 'Google Public DNS' + type: 'ping' + target: '8.8.8.8' +``` + +Configuration details: + +- `endpoints` is an array +- `name` is the friendly name for your endpoint +- `type` can be either `ping` or `fetch` and will perform the respective action on the `target` +- `target` is either a URL or server that you want to trigger a `type` action on diff --git a/plugins/devtools/api-report.md b/plugins/devtools/api-report.md new file mode 100644 index 0000000000..c9cfbf77e5 --- /dev/null +++ b/plugins/devtools/api-report.md @@ -0,0 +1,57 @@ +## API Report File for "@backstage/plugin-devtools" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { default as default_2 } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { TabProps } from '@material-ui/core'; + +// @public (undocumented) +export const ConfigContent: () => JSX.Element; + +// @public +export const DevToolsLayout: { + ({ children }: DevToolsLayoutProps): JSX.Element; + Route: (props: SubRoute) => null; +}; + +// @public (undocumented) +export type DevToolsLayoutProps = { + children?: default_2.ReactNode; +}; + +// @public (undocumented) +export const DevToolsPage: () => JSX.Element; + +// @public (undocumented) +export const devToolsPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {}, + {} +>; + +// @public (undocumented) +export const ExternalDependenciesContent: () => JSX.Element; + +// @public (undocumented) +export const InfoContent: () => JSX.Element; + +// @public (undocumented) +export type SubRoute = { + path: string; + title: string; + children: JSX.Element; + tabProps?: TabProps< + default_2.ElementType, + { + component?: default_2.ElementType; + } + >; +}; +``` diff --git a/plugins/devtools/dev/index.tsx b/plugins/devtools/dev/index.tsx new file mode 100644 index 0000000000..00c966a801 --- /dev/null +++ b/plugins/devtools/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { devToolsPlugin, DevToolsPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(devToolsPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/devtools', + }) + .render(); diff --git a/plugins/devtools/docs/devtools-config-tab.png b/plugins/devtools/docs/devtools-config-tab.png new file mode 100644 index 0000000000..b5c22f691b Binary files /dev/null and b/plugins/devtools/docs/devtools-config-tab.png differ diff --git a/plugins/devtools/docs/devtools-external-dependencies.png b/plugins/devtools/docs/devtools-external-dependencies.png new file mode 100644 index 0000000000..9356d83ef5 Binary files /dev/null and b/plugins/devtools/docs/devtools-external-dependencies.png differ diff --git a/plugins/devtools/docs/devtools-info-tab.png b/plugins/devtools/docs/devtools-info-tab.png new file mode 100644 index 0000000000..739045a77f Binary files /dev/null and b/plugins/devtools/docs/devtools-info-tab.png differ diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json new file mode 100644 index 0000000000..5997f79845 --- /dev/null +++ b/plugins/devtools/package.json @@ -0,0 +1,64 @@ +{ + "name": "@backstage/plugin-devtools", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/devtools" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-devtools-common": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", + "@backstage/theme": "workspace:^", + "@backstage/types": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.57", + "react-json-view": "^1.21.3", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.47.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/devtools/src/api/DevToolsApi.ts b/plugins/devtools/src/api/DevToolsApi.ts new file mode 100644 index 0000000000..ff58a4ea16 --- /dev/null +++ b/plugins/devtools/src/api/DevToolsApi.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core-plugin-api'; +import { + ConfigInfo, + DevToolsInfo, + ExternalDependency, +} from '@backstage/plugin-devtools-common'; + +export const devToolsApiRef = createApiRef({ + id: 'plugin.devtools.service', +}); + +export interface DevToolsApi { + getConfig(): Promise; + getExternalDependencies(): Promise; + getInfo(): Promise; +} diff --git a/plugins/devtools/src/api/DevToolsClient.ts b/plugins/devtools/src/api/DevToolsClient.ts new file mode 100644 index 0000000000..0be8294b3f --- /dev/null +++ b/plugins/devtools/src/api/DevToolsClient.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { + ConfigInfo, + DevToolsInfo, + ExternalDependency, +} from '@backstage/plugin-devtools-common'; +import { ResponseError } from '@backstage/errors'; +import { DevToolsApi } from './DevToolsApi'; + +export class DevToolsClient implements DevToolsApi { + private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; + + public constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { + this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; + } + + public async getConfig(): Promise { + const urlSegment = 'config'; + + const configInfo = await this.get(urlSegment); + return configInfo; + } + + public async getExternalDependencies(): Promise< + ExternalDependency[] | undefined + > { + const urlSegment = 'external-dependencies'; + + const externalDependencies = await this.get< + ExternalDependency[] | undefined + >(urlSegment); + return externalDependencies; + } + + public async getInfo(): Promise { + const urlSegment = 'info'; + + const info = await this.get(urlSegment); + return info; + } + + private async get(path: string): Promise { + const baseUrl = `${await this.discoveryApi.getBaseUrl('devtools')}/`; + const url = new URL(path, baseUrl); + + const { token } = await this.identityApi.getCredentials(); + const response = await fetch(url.toString(), { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return response.json() as Promise; + } +} diff --git a/plugins/devtools/src/api/index.ts b/plugins/devtools/src/api/index.ts new file mode 100644 index 0000000000..6bea0a07aa --- /dev/null +++ b/plugins/devtools/src/api/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './DevToolsApi'; +export * from './DevToolsClient'; diff --git a/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx b/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx new file mode 100644 index 0000000000..66129b5dae --- /dev/null +++ b/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx @@ -0,0 +1,95 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Progress, WarningPanel } from '@backstage/core-components'; +import { + Box, + createStyles, + makeStyles, + Paper, + Theme, + Typography, + useTheme, +} from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React from 'react'; +import ReactJson from 'react-json-view'; +import { useConfig } from '../../../hooks'; +import { ConfigError } from '@backstage/plugin-devtools-common'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + warningStyle: { + paddingBottom: theme.spacing(2), + }, + paperStyle: { + padding: theme.spacing(2), + }, + }), +); + +export const WarningContent = ({ error }: { error: ConfigError }) => { + if (!error.messages) { + return {error.message}; + } + + const messages = error.messages as string[]; + + return ( + + {messages.map(message => ( + {message} + ))} + + ); +}; + +/** @public */ +export const ConfigContent = () => { + const classes = useStyles(); + const theme = useTheme(); + const { configInfo, loading, error } = useConfig(); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + if (!configInfo) { + return Unable to load config data; + } + + return ( + + {configInfo && configInfo.error && ( + + + + + + )} + + + + + ); +}; diff --git a/plugins/devtools/src/components/Content/ConfigContent/index.ts b/plugins/devtools/src/components/Content/ConfigContent/index.ts new file mode 100644 index 0000000000..b1cc671de8 --- /dev/null +++ b/plugins/devtools/src/components/Content/ConfigContent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ConfigContent } from './ConfigContent'; diff --git a/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx b/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx new file mode 100644 index 0000000000..a5dd825fe5 --- /dev/null +++ b/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx @@ -0,0 +1,140 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Progress, + StatusError, + StatusOK, + StatusWarning, + Table, + TableColumn, +} from '@backstage/core-components'; +import { ExternalDependency } from '@backstage/plugin-devtools-common'; +import { + Box, + createStyles, + Grid, + makeStyles, + Paper, + Theme, + Typography, +} from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React from 'react'; +import { useExternalDependencies } from '../../../hooks'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + paperStyle: { + padding: theme.spacing(2), + }, + }), +); + +export const getExternalDependencyStatus = ( + result: Partial | undefined, +) => { + switch (result?.status) { + case 'Healthy': + return ( + + {result.status} + + ); + case 'Unhealthy': + return ( + + {`${result.status}`} + + ); + case undefined: + default: + return ( + + Unknown + + ); + } +}; + +const columns: TableColumn[] = [ + { + title: 'Name', + width: 'auto', + field: 'name', + }, + { + title: 'Target', + width: 'auto', + field: 'target', + }, + { + title: 'Type', + width: 'auto', + field: 'type', + }, + { + title: 'Status', + width: 'auto', + render: (row: Partial) => ( + + + + {getExternalDependencyStatus(row)} + + + {row.error && {row.error}} + + ), + }, +]; + +/** @public */ +export const ExternalDependenciesContent = () => { + const classes = useStyles(); + const { externalDependencies, loading, error } = useExternalDependencies(); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + if (!externalDependencies || externalDependencies.length === 0) { + return ( + + + No external dependencies found + + + ); + } + + return ( + + ); +}; diff --git a/plugins/devtools/src/components/Content/ExternalDependenciesContent/index.ts b/plugins/devtools/src/components/Content/ExternalDependenciesContent/index.ts new file mode 100644 index 0000000000..bf747f3065 --- /dev/null +++ b/plugins/devtools/src/components/Content/ExternalDependenciesContent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ExternalDependenciesContent } from './ExternalDependenciesContent'; diff --git a/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx b/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx new file mode 100644 index 0000000000..32f35600f2 --- /dev/null +++ b/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SvgIcon, SvgIconProps } from '@material-ui/core'; + +import React from 'react'; + +export const BackstageLogoIcon = (props: SvgIconProps) => ( + + + +); diff --git a/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx b/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx new file mode 100644 index 0000000000..7a0ebb3e06 --- /dev/null +++ b/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx @@ -0,0 +1,138 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Progress } from '@backstage/core-components'; +import { + Avatar, + Box, + createStyles, + Divider, + List, + ListItem, + ListItemAvatar, + ListItemText, + makeStyles, + Paper, + Theme, +} from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React from 'react'; +import { useInfo } from '../../../hooks'; +import { InfoDependenciesTable } from './InfoDependenciesTable'; +import DescriptionIcon from '@material-ui/icons/Description'; +import DeveloperBoardIcon from '@material-ui/icons/DeveloperBoard'; +import { BackstageLogoIcon } from './BackstageLogoIcon'; +import FileCopyIcon from '@material-ui/icons/FileCopy'; +import { DevToolsInfo } from '@backstage/plugin-devtools-common'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + paperStyle: { + marginBottom: theme.spacing(2), + }, + flexContainer: { + display: 'flex', + flexDirection: 'row', + padding: 0, + }, + copyButton: { + float: 'left', + margin: theme.spacing(2), + }, + }), +); + +const copyToClipboard = ({ about }: { about: DevToolsInfo | undefined }) => { + if (about) { + let formatted = `OS: ${about.operatingSystem}\nnode: ${about.nodeJsVersion}\nbackstage: ${about.backstageVersion}\nDependencies:\n`; + const deps = about.dependencies; + for (const key in deps) { + if (Object.prototype.hasOwnProperty.call(deps, key)) { + formatted = `${formatted} ${deps[key].name}: ${deps[key].versions}\n`; + } + } + window.navigator.clipboard.writeText(formatted); + } +}; + +/** @public */ +export const InfoContent = () => { + const classes = useStyles(); + const { about, loading, error } = useInfo(); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + copyToClipboard({ about }); + }} + className={classes.copyButton} + > + + + + + + + + + + + + ); +}; diff --git a/plugins/devtools/src/components/Content/InfoContent/InfoDependenciesTable.tsx b/plugins/devtools/src/components/Content/InfoContent/InfoDependenciesTable.tsx new file mode 100644 index 0000000000..e1ab59f279 --- /dev/null +++ b/plugins/devtools/src/components/Content/InfoContent/InfoDependenciesTable.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Table, TableColumn } from '@backstage/core-components'; +import { PackageDependency } from '@backstage/plugin-devtools-common'; + +import React from 'react'; + +const columns: TableColumn[] = [ + { + title: 'Name', + width: 'auto', + field: 'name', + defaultSort: 'asc', + }, + { + title: 'Versions', + width: 'auto', + field: 'versions', + }, +]; + +export const InfoDependenciesTable = ({ + infoDependencies, +}: { + infoDependencies: PackageDependency[] | undefined; +}) => { + return ( +
+ ); +}; diff --git a/plugins/devtools/src/components/Content/InfoContent/index.ts b/plugins/devtools/src/components/Content/InfoContent/index.ts new file mode 100644 index 0000000000..6d5bad53f8 --- /dev/null +++ b/plugins/devtools/src/components/Content/InfoContent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { InfoContent } from './InfoContent'; diff --git a/plugins/devtools/src/components/Content/index.ts b/plugins/devtools/src/components/Content/index.ts new file mode 100644 index 0000000000..f5bd64f7c0 --- /dev/null +++ b/plugins/devtools/src/components/Content/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './ConfigContent'; +export * from './InfoContent'; +export * from './ExternalDependenciesContent'; diff --git a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx new file mode 100644 index 0000000000..a5e4f2439b --- /dev/null +++ b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + devToolsConfigReadPermission, + devToolsInfoReadPermission, +} from '@backstage/plugin-devtools-common'; + +import { ConfigContent } from '../Content/ConfigContent'; +import { DevToolsLayout } from '../DevToolsLayout'; +import { InfoContent } from '../Content/InfoContent'; +import React from 'react'; +import { RequirePermission } from '@backstage/plugin-permission-react'; + +/** @public */ +export const DefaultDevToolsPage = () => ( + + + + + + + + + + + + +); diff --git a/plugins/devtools/src/components/DefaultDevToolsPage/index.ts b/plugins/devtools/src/components/DefaultDevToolsPage/index.ts new file mode 100644 index 0000000000..133dcf6e3a --- /dev/null +++ b/plugins/devtools/src/components/DefaultDevToolsPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { DefaultDevToolsPage } from './DefaultDevToolsPage'; diff --git a/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx new file mode 100644 index 0000000000..12cd3f7666 --- /dev/null +++ b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Header, Page, RoutedTabs } from '@backstage/core-components'; +import { + attachComponentData, + useElementFilter, +} from '@backstage/core-plugin-api'; +import { TabProps } from '@material-ui/core'; +import { default as React } from 'react'; + +/** @public */ +export type SubRoute = { + path: string; + title: string; + children: JSX.Element; + tabProps?: TabProps; +}; + +const dataKey = 'plugin.devtools.devtoolsLayoutRoute'; + +const Route: (props: SubRoute) => null = () => null; +attachComponentData(Route, dataKey, true); + +// This causes all mount points that are discovered within this route to use the path of the route itself +attachComponentData(Route, 'core.gatherMountPoints', true); + +/** @public */ +export type DevToolsLayoutProps = { + children?: React.ReactNode; +}; + +/** + * DevTools is a compound component, which allows you to define a custom layout + * + * @example + * ```jsx + * + * + *
This is rendered under /example/anything-here route
+ *
+ *
+ * ``` + * @public + */ +export const DevToolsLayout = ({ children }: DevToolsLayoutProps) => { + const routes = useElementFilter(children, elements => + elements + .selectByComponentData({ + key: dataKey, + withStrictError: + 'Child of DevToolsLayout must be an DevToolsLayout.Route', + }) + .getElements() + .map(child => child.props), + ); + + return ( + +
+ + + ); +}; + +DevToolsLayout.Route = Route; diff --git a/plugins/devtools/src/components/DevToolsLayout/index.tsx b/plugins/devtools/src/components/DevToolsLayout/index.tsx new file mode 100644 index 0000000000..0bef371b05 --- /dev/null +++ b/plugins/devtools/src/components/DevToolsLayout/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { DevToolsLayoutProps, SubRoute } from './DevToolsLayout'; +export { DevToolsLayout } from './DevToolsLayout'; diff --git a/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx new file mode 100644 index 0000000000..880efed68c --- /dev/null +++ b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { useOutlet } from 'react-router-dom'; +import { DefaultDevToolsPage } from '../DefaultDevToolsPage'; + +export const DevToolsPage = () => { + const outlet = useOutlet(); + + return <>{outlet || }; +}; diff --git a/plugins/devtools/src/components/DevToolsPage/index.ts b/plugins/devtools/src/components/DevToolsPage/index.ts new file mode 100644 index 0000000000..88d8bc06fa --- /dev/null +++ b/plugins/devtools/src/components/DevToolsPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { DevToolsPage } from './DevToolsPage'; diff --git a/plugins/devtools/src/components/index.ts b/plugins/devtools/src/components/index.ts new file mode 100644 index 0000000000..9938866bcc --- /dev/null +++ b/plugins/devtools/src/components/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './Content'; +export * from './DevToolsLayout'; diff --git a/plugins/devtools/src/hooks/index.ts b/plugins/devtools/src/hooks/index.ts new file mode 100644 index 0000000000..bb2e83d06f --- /dev/null +++ b/plugins/devtools/src/hooks/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { useConfig } from './useConfig'; +export { useExternalDependencies } from './useExternalDependencies'; +export { useInfo } from './useInfo'; diff --git a/plugins/devtools/src/hooks/useConfig.ts b/plugins/devtools/src/hooks/useConfig.ts new file mode 100644 index 0000000000..6703e7ea59 --- /dev/null +++ b/plugins/devtools/src/hooks/useConfig.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { devToolsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { ConfigInfo } from '@backstage/plugin-devtools-common'; + +export function useConfig(): { + configInfo?: ConfigInfo; + loading: boolean; + error?: Error; +} { + const api = useApi(devToolsApiRef); + const { value, loading, error } = useAsync(() => { + return api.getConfig(); + }, [api]); + + return { + configInfo: value, + loading, + error, + }; +} diff --git a/plugins/devtools/src/hooks/useExternalDependencies.ts b/plugins/devtools/src/hooks/useExternalDependencies.ts new file mode 100644 index 0000000000..df2f3e193b --- /dev/null +++ b/plugins/devtools/src/hooks/useExternalDependencies.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { devToolsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { ExternalDependency } from '@backstage/plugin-devtools-common'; + +export function useExternalDependencies(): { + externalDependencies?: ExternalDependency[]; + loading: boolean; + error?: Error; +} { + const api = useApi(devToolsApiRef); + const { value, loading, error } = useAsync(() => { + return api.getExternalDependencies(); + }, [api]); + + return { + externalDependencies: value, + loading, + error, + }; +} diff --git a/plugins/devtools/src/hooks/useInfo.ts b/plugins/devtools/src/hooks/useInfo.ts new file mode 100644 index 0000000000..ad700cb3fe --- /dev/null +++ b/plugins/devtools/src/hooks/useInfo.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { devToolsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { DevToolsInfo } from '@backstage/plugin-devtools-common'; + +export function useInfo(): { + about?: DevToolsInfo; + loading: boolean; + error?: Error; +} { + const api = useApi(devToolsApiRef); + const { value, loading, error } = useAsync(() => { + return api.getInfo(); + }, [api]); + + return { + about: value, + loading, + error, + }; +} diff --git a/plugins/devtools/src/index.ts b/plugins/devtools/src/index.ts new file mode 100644 index 0000000000..1077a2518c --- /dev/null +++ b/plugins/devtools/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Backstage DevTools frontend plugin with a tab that shows package dependencies and another tab that shows your current configuration. + * + * @packageDocumentation + */ + +export { devToolsPlugin, DevToolsPage } from './plugin'; +export * from './components'; diff --git a/plugins/devtools/src/plugin.test.ts b/plugins/devtools/src/plugin.test.ts new file mode 100644 index 0000000000..62f4ae5a61 --- /dev/null +++ b/plugins/devtools/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { devToolsPlugin } from './plugin'; + +describe('devtools', () => { + it('should export plugin', () => { + expect(devToolsPlugin).toBeDefined(); + }); +}); diff --git a/plugins/devtools/src/plugin.ts b/plugins/devtools/src/plugin.ts new file mode 100644 index 0000000000..df2aca3447 --- /dev/null +++ b/plugins/devtools/src/plugin.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createApiFactory, + createPlugin, + createRoutableExtension, + discoveryApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { devToolsApiRef, DevToolsClient } from './api'; + +import { rootRouteRef } from './routes'; + +/** @public */ +export const devToolsPlugin = createPlugin({ + id: 'devtools', + apis: [ + createApiFactory({ + api: devToolsApiRef, + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new DevToolsClient({ discoveryApi, identityApi }), + }), + ], + routes: { + root: rootRouteRef, + }, +}); + +/** @public */ +export const DevToolsPage = devToolsPlugin.provide( + createRoutableExtension({ + name: 'DevToolsPage', + component: () => + import('./components/DevToolsPage').then(m => m.DevToolsPage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/devtools/src/routes.ts b/plugins/devtools/src/routes.ts new file mode 100644 index 0000000000..7825f928be --- /dev/null +++ b/plugins/devtools/src/routes.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'devtools', +}); diff --git a/plugins/devtools/src/setupTests.ts b/plugins/devtools/src/setupTests.ts new file mode 100644 index 0000000000..7a459ed24e --- /dev/null +++ b/plugins/devtools/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; diff --git a/plugins/dynatrace/CHANGELOG.md b/plugins/dynatrace/CHANGELOG.md index 5ede228812..bfb0eac993 100644 --- a/plugins/dynatrace/CHANGELOG.md +++ b/plugins/dynatrace/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-dynatrace +## 5.0.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 5.0.0-next.1 ### Patch Changes diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json index b7c8244753..ab241ec0ad 100644 --- a/plugins/dynatrace/package.json +++ b/plugins/dynatrace/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-dynatrace", - "version": "5.0.0-next.1", + "version": "5.0.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/entity-feedback/CHANGELOG.md b/plugins/entity-feedback/CHANGELOG.md index 3f4575ca8a..d32012518e 100644 --- a/plugins/entity-feedback/CHANGELOG.md +++ b/plugins/entity-feedback/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-entity-feedback +## 0.2.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.1-next.1 ### Patch Changes diff --git a/plugins/entity-feedback/package.json b/plugins/entity-feedback/package.json index 0c15a0f3c8..0b90caea9d 100644 --- a/plugins/entity-feedback/package.json +++ b/plugins/entity-feedback/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-entity-feedback", - "version": "0.2.1-next.1", + "version": "0.2.1-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/entity-validation/CHANGELOG.md b/plugins/entity-validation/CHANGELOG.md index 9b63d46c5b..6d423d04bf 100644 --- a/plugins/entity-validation/CHANGELOG.md +++ b/plugins/entity-validation/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-entity-validation +## 0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.3-next.1 ### Patch Changes diff --git a/plugins/entity-validation/package.json b/plugins/entity-validation/package.json index 674c5c6a56..950687a884 100644 --- a/plugins/entity-validation/package.json +++ b/plugins/entity-validation/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-entity-validation", - "version": "0.1.3-next.1", + "version": "0.1.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 9b076afbd3..241463ac5c 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.2.0-next.2 + +### Minor Changes + +- 2c5661f3899: Allow endpoint configuration for sqs, enabling use of localstack for testing. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.7 + ## 0.1.7-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 0125d5e5b4..f9b3fd071b 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.1.7-next.1", + "version": "0.2.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { - "@aws-sdk/client-sqs": "^3.208.0", + "@aws-sdk/client-sqs": "^3.310.0", "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", @@ -46,7 +46,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@aws-sdk/types": "^3.208.0", + "@aws-sdk/types": "^3.310.0", "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts index e649e2ee83..4f4e6ea55d 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts @@ -68,7 +68,11 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { VisibilityTimeout: config.visibilityTimeout?.as('seconds'), WaitTimeSeconds: config.pollingWaitTime.as('seconds'), }; - this.sqs = new SQSClient({ region: config.region }); + + this.sqs = new SQSClient({ + region: config.region, + endpoint: config.endpoint, + }); this.queueUrl = config.queueUrl; this.taskTimeoutSeconds = config.timeout.as('seconds'); diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts b/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts index 1fa9df4ce3..6d7a08d7a7 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts @@ -79,6 +79,7 @@ describe('readConfig', () => { url: 'https://fake1.queue.url', visibilityTimeout: { minutes: 5 }, waitTime: { seconds: 10 }, + endpoint: 'https://fake.endpoint.url', }, timeout: { minutes: 5 }, waitTimeAfterEmptyReceive: { seconds: 30 }, @@ -97,6 +98,7 @@ describe('readConfig', () => { expect(publisherConfigs[0].topic).toEqual('fake1'); expect(publisherConfigs[0].region).toEqual('eu-west-1'); expect(publisherConfigs[0].queueUrl).toEqual('https://fake1.queue.url'); + expect(publisherConfigs[0].endpoint).toEqual('https://fake.endpoint.url'); expect(publisherConfigs[0].pollingWaitTime.as('seconds')).toBe(10); expect(publisherConfigs[0].timeout.as('seconds')).toBe(300); expect(publisherConfigs[0].waitTimeAfterEmptyReceive.as('seconds')).toBe( diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/config.ts b/plugins/events-backend-module-aws-sqs/src/publisher/config.ts index c6b74ea8d3..eeb650ff6c 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/config.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/config.ts @@ -31,6 +31,7 @@ export interface AwsSqsEventSourceConfig { topic: string; visibilityTimeout?: Duration; waitTimeAfterEmptyReceive: Duration; + endpoint?: string; } // TODO(pjungermann): validation could be improved similar to `convertToHumanDuration` at @backstage/backend-tasks @@ -74,6 +75,7 @@ export function readConfig(config: Config): AwsSqsEventSourceConfig[] { } const queueUrl = topicConfig.getString('queue.url'); const region = topicConfig.getString('queue.region'); + const endpoint = topicConfig.getOptionalString('queue.endpoint'); const visibilityTimeout = readOptionalDuration( topicConfig, 'queue.visibilityTimeout', @@ -106,6 +108,7 @@ export function readConfig(config: Config): AwsSqsEventSourceConfig[] { return { pollingWaitTime, queueUrl, + endpoint, region, timeout, topic, diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index 87cc779cde..c12164fa62 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,14 @@ # @internal/plugin-todo-list +## 1.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 1.0.13-next.0 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index fcd193bd5f..4ee26dea2c 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.13-next.0", + "version": "1.0.13-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 3ad9bdb917..36d04b4231 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-explore +## 0.4.3-next.2 + +### Patch Changes + +- 4851581deb6: Display the title of the entity on the explore card if present, otherwise stick to the name +- a6025e25d99: Updated the example code in the "Customization" section of the README to make the imports match the components used. +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/plugin-explore-react@0.0.28 + - @backstage/core-plugin-api@1.5.1 + ## 0.4.3-next.1 ### Patch Changes diff --git a/plugins/explore/README.md b/plugins/explore/README.md index a1cb273aab..24e6496cc3 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -104,11 +104,11 @@ Create a custom explore page in ```tsx import { - DomainExplorerContent, + CatalogKindExploreContent, ExploreLayout, } from '@backstage/plugin-explore'; import React from 'react'; -import { InnserSourceExplorerContent } from './InnserSourceExplorerContent'; +import { InnerSourceExploreContent } from './InnerSourceExploreContent'; export const ExplorePage = () => { return ( @@ -123,7 +123,7 @@ export const ExplorePage = () => { - + ); diff --git a/plugins/explore/package.json b/plugins/explore/package.json index bbe114c1f0..21c8fcc58b 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore", "description": "A Backstage plugin for building an exploration page of your software ecosystem", - "version": "0.4.3-next.1", + "version": "0.4.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx index df9b84820a..b66e30c652 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx @@ -17,6 +17,7 @@ import { DomainEntity } from '@backstage/catalog-model'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { DomainCard } from './DomainCard'; @@ -51,4 +52,33 @@ describe('', () => { '/catalog/default/domain/artists', ); }); + + it('renders a domain card with custom title', async () => { + const entity: DomainEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'artists', + title: 'my Custom Title', + }, + spec: { + owner: 'guest', + }, + }; + const { getByText } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(getByText('Explore').parentElement).toHaveAttribute( + 'href', + '/catalog/default/domain/artists', + ); + expect(getByText('my Custom Title')).toBeInTheDocument(); + expect(screen.queryByText('artists')).not.toBeInTheDocument(); + }); }); diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx index 0d37e3e273..c70df46b1a 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx @@ -53,7 +53,10 @@ export const DomainCard = (props: { entity: DomainEntity }) => { return ( - + {entity.metadata.tags?.length ? ( diff --git a/plugins/explore/src/components/EntityCard/EntityCard.test.tsx b/plugins/explore/src/components/EntityCard/EntityCard.test.tsx index 4b006e347f..cebd5af3d6 100644 --- a/plugins/explore/src/components/EntityCard/EntityCard.test.tsx +++ b/plugins/explore/src/components/EntityCard/EntityCard.test.tsx @@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; +import { screen } from '@testing-library/react'; import { EntityCard } from './EntityCard'; describe('', () => { @@ -51,4 +52,29 @@ describe('', () => { '/catalog/default/domain/artists', ); }); + + it('renders an entity card with display title', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'artists', + title: 'New Title', + }, + spec: { + owner: 'guest', + }, + }; + const { getByText } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(getByText('New Title')).toBeInTheDocument(); + expect(screen.queryByText('artists')).not.toBeInTheDocument(); + }); }); diff --git a/plugins/explore/src/components/EntityCard/EntityCard.tsx b/plugins/explore/src/components/EntityCard/EntityCard.tsx index 0a5908df38..9589d4721e 100644 --- a/plugins/explore/src/components/EntityCard/EntityCard.tsx +++ b/plugins/explore/src/components/EntityCard/EntityCard.tsx @@ -53,7 +53,10 @@ export const EntityCard = (props: { entity: Entity }) => { return ( - + {entity.metadata.tags?.length ? ( diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index 27c6a1b62e..9822bb684e 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-firehydrant +## 0.2.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.2-next.1 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index d5e136229c..260b3ad47f 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-firehydrant", "description": "A Backstage plugin that integrates towards FireHydrant", - "version": "0.2.2-next.1", + "version": "0.2.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index 0399ad0ce2..ea411c0558 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-fossa +## 0.2.50-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.50-next.1 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 985202a403..a1b9e48f3b 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-fossa", "description": "A Backstage plugin that integrates towards FOSSA", - "version": "0.2.50-next.1", + "version": "0.2.50-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/gcalendar/CHANGELOG.md b/plugins/gcalendar/CHANGELOG.md index 1c23885154..b211ebd95a 100644 --- a/plugins/gcalendar/CHANGELOG.md +++ b/plugins/gcalendar/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-gcalendar +## 0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.14-next.0 ### Patch Changes diff --git a/plugins/gcalendar/package.json b/plugins/gcalendar/package.json index 0cc77cabad..10375e2e3f 100644 --- a/plugins/gcalendar/package.json +++ b/plugins/gcalendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcalendar", - "version": "0.3.14-next.0", + "version": "0.3.14-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/gcalendar/src/hooks/useSignIn.ts b/plugins/gcalendar/src/hooks/useSignIn.ts index 351707b54e..3dbc778cf1 100644 --- a/plugins/gcalendar/src/hooks/useSignIn.ts +++ b/plugins/gcalendar/src/hooks/useSignIn.ts @@ -25,7 +25,10 @@ export const useSignIn = () => { const signIn = useCallback( async (optional = false) => { const token = await authApi.getAccessToken( - 'https://www.googleapis.com/auth/calendar.readonly', + [ + 'https://www.googleapis.com/auth/calendar.readonly', + 'https://www.googleapis.com/auth/userinfo.email', + ], { optional, instantPopup: !optional, diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index 7a5ea25c0d..8254e86dbc 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-gcp-projects +## 0.3.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.37-next.0 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 039bcd094c..9857a6ccf1 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gcp-projects", "description": "A Backstage plugin that helps you manage projects in GCP", - "version": "0.3.37-next.0", + "version": "0.3.37-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index 976585c656..7899cff4fe 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-git-release-manager +## 0.3.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.31-next.1 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 7bc4df059e..a73c35aead 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.3.31-next.1", + "version": "0.3.31-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 09b205c9f1..8b64c47ac8 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-github-actions +## 0.5.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.5.18-next.1 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 25fac2b774..a2c25741be 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.5.18-next.1", + "version": "0.5.18-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index 41475febd3..ba1d67aece 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-deployments +## 0.1.49-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.49-next.1 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index dcb4156f9d..c9df936591 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-deployments", "description": "A Backstage plugin that integrates towards GitHub Deployments", - "version": "0.1.49-next.1", + "version": "0.1.49-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/github-issues/CHANGELOG.md b/plugins/github-issues/CHANGELOG.md index d1e2103800..ee2e47570d 100644 --- a/plugins/github-issues/CHANGELOG.md +++ b/plugins/github-issues/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-github-issues +## 0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.7-next.1 ### Patch Changes diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json index ed982410e9..8a12aeb2de 100644 --- a/plugins/github-issues/package.json +++ b/plugins/github-issues/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-issues", - "version": "0.2.7-next.1", + "version": "0.2.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/github-pull-requests-board/CHANGELOG.md b/plugins/github-pull-requests-board/CHANGELOG.md index 7421c6e015..53bcb9b570 100644 --- a/plugins/github-pull-requests-board/CHANGELOG.md +++ b/plugins/github-pull-requests-board/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-github-pull-requests-board +## 0.1.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.12-next.1 ### Patch Changes diff --git a/plugins/github-pull-requests-board/package.json b/plugins/github-pull-requests-board/package.json index e805cd99ce..824d265abe 100644 --- a/plugins/github-pull-requests-board/package.json +++ b/plugins/github-pull-requests-board/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-pull-requests-board", "description": "A Backstage plugin that allows you to see all open Pull Requests for all the repositories owned by your team", - "version": "0.1.12-next.1", + "version": "0.1.12-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index 99122116fb..c5eba8def3 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-gitops-profiles +## 0.3.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.36-next.0 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 2bb6f145a4..e721682976 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gitops-profiles", "description": "A Backstage plugin that helps you manage GitOps profiles", - "version": "0.3.36-next.0", + "version": "0.3.36-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md index 14a0149ca5..9eb9964c96 100644 --- a/plugins/gocd/CHANGELOG.md +++ b/plugins/gocd/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-gocd +## 0.1.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.24-next.1 ### Patch Changes diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json index 2e41e2c217..c8fca38e23 100644 --- a/plugins/gocd/package.json +++ b/plugins/gocd/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gocd", "description": "A Backstage plugin that integrates towards GoCD", - "version": "0.1.24-next.1", + "version": "0.1.24-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index e831845d49..3d49c1b29f 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-graphiql +## 0.2.50-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.50-next.0 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 08fc2a6ec0..219551e6e7 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.50-next.0", + "version": "0.2.50-next.1", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/graphql-voyager/CHANGELOG.md b/plugins/graphql-voyager/CHANGELOG.md index 77586073af..c47af7b006 100644 --- a/plugins/graphql-voyager/CHANGELOG.md +++ b/plugins/graphql-voyager/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-graphql-voyager +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.3-next.0 ### Patch Changes diff --git a/plugins/graphql-voyager/package.json b/plugins/graphql-voyager/package.json index d356dee1c5..c459e4fbf8 100644 --- a/plugins/graphql-voyager/package.json +++ b/plugins/graphql-voyager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphql-voyager", "description": "Backstage plugin for GraphQL Voyager", - "version": "0.1.3-next.0", + "version": "0.1.3-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 868b1a6c3b..3996c77103 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-home +## 0.5.2-next.2 + +### Patch Changes + +- acca8966465: Remove object-hash dependency +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.5.2-next.1 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index ef9267234b..12e55d80cd 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-home", "description": "A Backstage plugin that helps you build a home page", - "version": "0.5.2-next.1", + "version": "0.5.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -48,7 +48,6 @@ "@rjsf/validator-ajv8": "5.6.0", "@types/react": "^16.13.1 || ^17.0.0", "lodash": "^4.17.21", - "object-hash": "^3.0.0", "react-grid-layout": "^1.3.4", "react-resizable": "^3.0.4", "react-use": "^17.2.4", diff --git a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx index 232fbd12c2..6af9044cbd 100644 --- a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx +++ b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx @@ -46,7 +46,6 @@ import { CardConfig } from '../../extensions'; // eslint-disable-next-line new-cap const ResponsiveGrid = WidthProvider(Responsive); -const hash = require('object-hash'); const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -336,14 +335,14 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { ...widget.component.props, ...(w.settings ?? {}), }; - const propsHash = hash(widgetProps, {}); + return (
- + {editMode && ( { />, ); - expect(rendered.getByText('09:10')).toBeInTheDocument(); + expect(rendered.getByText('09:10')).toHaveAttribute('datetime', '09:10'); }); }); diff --git a/plugins/home/src/homePageComponents/HeaderWorldClock/HeaderWorldClock.tsx b/plugins/home/src/homePageComponents/HeaderWorldClock/HeaderWorldClock.tsx index a45a51e0ad..394af1a27b 100644 --- a/plugins/home/src/homePageComponents/HeaderWorldClock/HeaderWorldClock.tsx +++ b/plugins/home/src/homePageComponents/HeaderWorldClock/HeaderWorldClock.tsx @@ -23,8 +23,9 @@ const timeFormat: Intl.DateTimeFormatOptions = { }; type TimeObj = { - time: string; label: string; + value: string; + dateTime: string; }; /** @public */ @@ -65,8 +66,14 @@ function getTimes( label = 'GMT'; } - const time = d.toLocaleTimeString(lang, options); - clocks.push({ time, label }); + const value = d.toLocaleTimeString(lang, options); + const dateTime = d.toLocaleTimeString(lang, { + timeZone: options.timeZone, + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + clocks.push({ label, value, dateTime }); } return clocks; @@ -129,9 +136,9 @@ export const HeaderWorldClock = (props: { <> {clocks.map(clock => ( {clock.value}} /> ))} diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index 0f5b044bfb..a1fb6e375d 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-ilert +## 0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.7-next.1 ### Patch Changes diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 17e94d54e5..0e08159481 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-ilert", "description": "A Backstage plugin that integrates towards iLert", - "version": "0.2.7-next.1", + "version": "0.2.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 4f58776df7..07624593c3 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -36,6 +36,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "jenkins": "^1.0.0", + "node-fetch": "^2.6.7", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index c89a3866c7..8e80de34a3 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -19,9 +19,10 @@ import jenkins from 'jenkins'; import { JenkinsInfo } from './jenkinsInfoProvider'; import { JenkinsBuild, JenkinsProject } from '../types'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; -import { NotAllowedError } from '@backstage/errors'; +import fetch, { Response } from 'node-fetch'; jest.mock('jenkins'); +jest.mock('node-fetch'); const mockedJenkinsClient = { job: { get: jest.fn(), @@ -54,6 +55,11 @@ const fakePermissionApi = { describe('JenkinsApi', () => { const jenkinsApi = new JenkinsApiImpl(fakePermissionApi); + const mockFetch = fetch as jest.MockedFunction; + + afterEach(() => { + jest.clearAllMocks(); + }); describe('getProjects', () => { const project: JenkinsProject = { @@ -689,55 +695,63 @@ describe('JenkinsApi', () => { buildNumber, ); }); - it('buildProject', async () => { - await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef); + it('getBuildUrl', async () => { + const jenkinsApiProto = Object.getPrototypeOf(jenkinsApi); + const buildUrl = jenkinsApiProto.getBuildUrl( + jenkinsInfo, + jobFullName, + buildNumber, + ); + expect(buildUrl).toEqual( + 'https://jenkins.example.com/job/example-jobName/job/foo/19', + ); - expect(mockedJenkins).toHaveBeenCalledWith({ - baseUrl: jenkinsInfo.baseUrl, - headers: jenkinsInfo.headers, - promisify: true, - }); - expect(mockedJenkinsClient.job.build).toHaveBeenCalledWith(jobFullName); + const buildUrlTriple = jenkinsApiProto.getBuildUrl( + jenkinsInfo, + 'example-jobName/foo/bar', + buildNumber, + ); + expect(buildUrlTriple).toEqual( + 'https://jenkins.example.com/job/example-jobName/job/foo/job/bar/19', + ); }); - - it('buildProject should fail if it does not have required permissions', async () => { - fakePermissionApi.authorize.mockResolvedValueOnce([ - { - result: AuthorizeResult.DENY, - }, - ]); - - await expect(() => - jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef), - ).rejects.toThrow(NotAllowedError); - }); - - it('buildProject should succeed if it have required permissions', async () => { - fakePermissionApi.authorize.mockResolvedValueOnce([ - { - result: AuthorizeResult.ALLOW, - }, - ]); - - await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef); - expect(mockedJenkins).toHaveBeenCalledWith({ - baseUrl: jenkinsInfo.baseUrl, - headers: jenkinsInfo.headers, - promisify: true, + describe('rebuildProject', () => { + it('successfully rebuilds', async () => { + mockFetch.mockResolvedValueOnce({ status: 200 } as Response); + const status = await jenkinsApi.rebuildProject( + jenkinsInfo, + jobFullName, + buildNumber, + resourceRef, + ); + expect(status).toEqual(200); }); - expect(mockedJenkinsClient.job.build).toHaveBeenCalledWith(jobFullName); - }); - - it('buildProject with crumbIssuer option', async () => { - const info: JenkinsInfo = { ...jenkinsInfo, crumbIssuer: true }; - await jenkinsApi.buildProject(info, jobFullName, resourceRef); - - expect(mockedJenkins).toHaveBeenCalledWith({ - baseUrl: jenkinsInfo.baseUrl, - headers: jenkinsInfo.headers, - promisify: true, - crumbIssuer: true, + it('fails to rebuild', async () => { + mockFetch.mockResolvedValueOnce({ status: 401 } as Response); + const status = await jenkinsApi.rebuildProject( + jenkinsInfo, + jobFullName, + buildNumber, + resourceRef, + ); + expect(status).toEqual(401); + }); + + it('should fail if it does not have required permissions', async () => { + fakePermissionApi.authorize.mockResolvedValueOnce([ + { + result: AuthorizeResult.DENY, + }, + ]); + + mockFetch.mockResolvedValueOnce({ status: 200 } as Response); + const status = await jenkinsApi.rebuildProject( + jenkinsInfo, + jobFullName, + buildNumber, + resourceRef, + ); + expect(status).toEqual(401); }); - expect(mockedJenkinsClient.job.build).toHaveBeenCalledWith(jobFullName); }); }); diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index e14437fa68..85271462eb 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -28,7 +28,7 @@ import { PermissionEvaluator, } from '@backstage/plugin-permission-common'; import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; -import { NotAllowedError } from '@backstage/errors'; +import fetch, { HeaderInit } from 'node-fetch'; export class JenkinsApiImpl { private static readonly lastBuildTreeSpec = `lastBuild[ @@ -144,14 +144,13 @@ export class JenkinsApiImpl { * Trigger a build of a project * @see ../../../jenkins/src/api/JenkinsApi.ts#retry */ - async buildProject( + async rebuildProject( jenkinsInfo: JenkinsInfo, jobFullName: string, + buildNumber: number, resourceRef: string, options?: { token?: string }, - ) { - const client = await JenkinsApiImpl.getClient(jenkinsInfo); - + ): Promise { if (this.permissionApi) { const response = await this.permissionApi.authorize( [{ permission: jenkinsExecutePermission, resourceRef }], @@ -160,16 +159,19 @@ export class JenkinsApiImpl { // permission api returns always at least one item, we need to check only one result since we do not expect any additional results const { result } = response[0]; if (result === AuthorizeResult.DENY) { - throw new NotAllowedError(); + return 401; } } - // looks like the current SDK only supports triggering a new build - // can't see any support for replay (re-running the specific build with the same SCM info) + const buildUrl = this.getBuildUrl(jenkinsInfo, jobFullName, buildNumber); - // Note Jenkins itself has concepts of rebuild and replay on a job. - // The latter should be possible to trigger with a POST to /replay/rebuild - await client.job.build(jobFullName); + // the current SDK only supports triggering a new build + // replay the job by triggering request directly from Jenkins api + const response = await fetch(`${buildUrl}/replay/rebuild`, { + method: 'post', + headers: jenkinsInfo.headers as HeaderInit, + }); + return response.status; } // private helper methods @@ -318,4 +320,13 @@ export class JenkinsApiImpl { }) .pop(); } + + private getBuildUrl( + jenkinsInfo: JenkinsInfo, + jobFullName: string, + buildId: number, + ): string { + const jobs = jobFullName.split('/'); + return `${jenkinsInfo.baseUrl}/job/${jobs.join('/job/')}/${buildId}`; + } } diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts index df4c3b5a11..484bfa273f 100644 --- a/plugins/jenkins-backend/src/service/router.ts +++ b/plugins/jenkins-backend/src/service/router.ts @@ -145,9 +145,10 @@ export async function createRouter( ); router.post( - '/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber::rebuild', + '/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber', async (request, response) => { - const { namespace, kind, name, jobFullName } = request.params; + const { namespace, kind, name, jobFullName, buildNumber } = + request.params; const token = getBearerTokenFromAuthorizationHeader( request.header('authorization'), ); @@ -162,10 +163,16 @@ export async function createRouter( }); const resourceRef = stringifyEntityRef({ kind, namespace, name }); - await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef, { - token, - }); - response.json({}); + const status = await jenkinsApi.rebuildProject( + jenkinsInfo, + jobFullName, + parseInt(buildNumber, 10), + resourceRef, + { + token, + }, + ); + response.json({}).status(status); }, ); router.use(errorHandler()); diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 6b900dcb18..9d617e1d39 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-jenkins +## 0.7.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.7.17-next.1 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index e57c1c77f2..6d8d50d26b 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.7.17-next.1", + "version": "0.7.17-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins/src/assets/folder-results.png b/plugins/jenkins/src/assets/folder-results.png index 2c14d5d927..340b03549d 100644 Binary files a/plugins/jenkins/src/assets/folder-results.png and b/plugins/jenkins/src/assets/folder-results.png differ diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx index 1bb6dbaefc..f2d108eaf7 100644 --- a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -18,6 +18,7 @@ import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha'; import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; +import VisibilityIcon from '@material-ui/icons/Visibility'; import { default as React, useState } from 'react'; import { Project } from '../../../../api/JenkinsApi'; import JenkinsLogo from '../../../../assets/JenkinsLogo.svg'; @@ -203,16 +204,23 @@ const generatedColumns: TableColumn[] = [ }; return ( - - <> - {isLoadingRebuild && } - {!isLoadingRebuild && ( +
+ {row.lastBuild?.url && ( + + + + + + )} + {isLoadingRebuild && } + {!isLoadingRebuild && ( + - )} - - + + )} +
); }; return ; diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 42f46a13a7..56e8a42053 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kafka +## 0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.18-next.1 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index c4eebb6446..4459b75b18 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.3.18-next.1", + "version": "0.3.18-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index fd6a844304..d7ac1585f8 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes-backend +## 0.11.0-next.2 + +### Patch Changes + +- 05f1d74539d: Kubernetes clusters now support `authProvider: aks`. When configured this way, + the `retrieveObjectsByServiceId` action will use the `auth.aks` value in the + request body as a bearer token to authenticate with Kubernetes. +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.3-next.0 + - @backstage/config@1.0.7 + - @backstage/integration-aws-node@0.1.2 + ## 0.11.0-next.1 ### Minor Changes diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 668061cd31..ad8ff32891 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -21,6 +21,15 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import type { RequestHandler } from 'express'; import { TokenCredential } from '@azure/identity'; +// @public (undocumented) +export class AksKubernetesAuthTranslator { + // (undocumented) + decorateClusterDetailsWithAuth( + clusterDetails: ClusterDetails, + auth: KubernetesRequestAuth, + ): Promise; +} + // @public (undocumented) export interface AWSClusterDetails extends ClusterDetails { // (undocumented) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 5e254f48e0..a45141b47d 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.11.0-next.1", + "version": "0.11.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -50,7 +50,7 @@ }, "dependencies": { "@aws-crypto/sha256-js": "^3.0.0", - "@aws-sdk/credential-providers": "^3.208.0", + "@aws-sdk/credential-providers": "^3.310.0", "@aws-sdk/signature-v4": "^3.310.0", "@azure/identity": "^2.0.4", "@backstage/backend-common": "workspace:^", @@ -70,7 +70,6 @@ "@kubernetes/client-node": "0.18.1", "@types/express": "^4.17.6", "@types/luxon": "^3.0.0", - "aws4": "^1.11.0", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 5d8fefc5d2..7f545a95de 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -256,4 +256,19 @@ describe('ConfigClusterLocator', () => { }, ]); }); + + it('supports aks authProvider', async () => { + const cluster = { + name: 'aks-cluster', + url: 'https://aks.test', + authProvider: 'aks', + }; + const sut = ConfigClusterLocator.fromConfig( + new ConfigReader({ clusters: [cluster] }), + ); + + const result = await sut.getClusters(); + + expect(result).toMatchObject([cluster]); + }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index b0143c2010..a759cb4bb0 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -76,6 +76,9 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { case 'googleServiceAccount': { return clusterDetails; } + case 'aks': { + return clusterDetails; + } default: { throw new Error( `authProvider "${authProvider}" has no config associated with it`, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts new file mode 100644 index 0000000000..d0d3cf87bb --- /dev/null +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AksKubernetesAuthTranslator } from './AksKubernetesAuthTranslator'; + +describe('AksKubernetesAuthTranslator', () => { + it('uses auth.aks value as bearer token', async () => { + const translator = new AksKubernetesAuthTranslator(); + + const details = await translator.decorateClusterDetailsWithAuth( + { name: '', authProvider: 'aks', url: '' }, + { aks: 'aksToken' }, + ); + + expect(details.serviceAccountToken).toBe('aksToken'); + }); +}); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts new file mode 100644 index 0000000000..13d784293e --- /dev/null +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClusterDetails } from '../types/types'; +import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; + +/** + * + * @public + */ +export class AksKubernetesAuthTranslator { + async decorateClusterDetailsWithAuth( + clusterDetails: ClusterDetails, + auth: KubernetesRequestAuth, + ): Promise { + return { ...clusterDetails, serviceAccountToken: auth.aks }; + } +} diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts index 4ee83b0da0..71dcedfee6 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './AksKubernetesAuthTranslator'; export * from './AwsIamKubernetesAuthTranslator'; export * from './AzureIdentityKubernetesAuthTranslator'; export * from './GoogleKubernetesAuthTranslator'; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 75b619b412..8f9d0769ed 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -33,6 +33,7 @@ import { GoogleServiceAccountAuthTranslator, AzureIdentityKubernetesAuthTranslator, OidcKubernetesAuthTranslator, + AksKubernetesAuthTranslator, } from '../kubernetes-auth-translator'; import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; @@ -355,6 +356,7 @@ export class KubernetesBuilder { protected buildAuthTranslatorMap() { this.authTranslatorMap = { google: new GoogleKubernetesAuthTranslator(), + aks: new AksKubernetesAuthTranslator(), aws: new AwsIamKubernetesAuthTranslator({ config: this.env.config }), azure: new AzureIdentityKubernetesAuthTranslator(this.env.logger), serviceAccount: new NoopKubernetesAuthTranslator(), diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 57eb29f0a7..97a0daa074 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -15,7 +15,7 @@ */ import 'buffer'; -import { getVoidLogger } from '@backstage/backend-common'; +import { errorHandler, getVoidLogger } from '@backstage/backend-common'; import { NotFoundError } from '@backstage/errors'; import { getMockReq, getMockRes } from '@jest-mock/express'; import type { Request } from 'express'; @@ -35,7 +35,12 @@ import { AuthorizeResult, PermissionEvaluator, } from '@backstage/plugin-permission-common'; -import { KubernetesAuthTranslator } from '../kubernetes-auth-translator'; +import { + KubernetesAuthTranslator, + NoopKubernetesAuthTranslator, +} from '../kubernetes-auth-translator'; +import Router from 'express-promise-router'; +import { LocalKubectlProxyClusterLocator } from '../cluster-locator/LocalKubectlProxyLocator'; describe('KubernetesProxy', () => { let proxy: KubernetesProxy; @@ -131,10 +136,9 @@ describe('KubernetesProxy', () => { authProvider: 'serviceAccount', } as ClusterDetails); - const app = express().use( - '/mountpath', - proxy.createRequestHandler({ permissionApi }), - ); + const router = Router(); + router.use('/mountpath', proxy.createRequestHandler({ permissionApi })); + const app = express().use(router); const requestPromise = request(app) .get('/mountpath/api') .set(HEADER_KUBERNETES_CLUSTER, 'cluster1'); @@ -151,7 +155,7 @@ describe('KubernetesProxy', () => { expect(response.body).toStrictEqual(apiResponse); }); - it('should default to using a provided authorization header', async () => { + it('should default to using a authTranslator provided serviceAccountToken as authorization headers to kubeapi when backstage-kubernetes-auth field is not provided', async () => { worker.use( rest.get( 'https://localhost:9999/api/v1/namespaces', @@ -160,7 +164,10 @@ describe('KubernetesProxy', () => { return res(ctx.status(401)); } - if (req.headers.get('Authorization') !== 'my-token') { + if ( + req.headers.get('Authorization') !== + 'Bearer translator-provided-token' + ) { return res(ctx.status(403)); } @@ -192,18 +199,17 @@ describe('KubernetesProxy', () => { authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: 'random-token', + serviceAccountToken: 'translator-provided-token', authProvider: 'serviceAccount', } as ClusterDetails); - const app = express().use( - '/mountpath', - proxy.createRequestHandler({ permissionApi }), - ); + const router = Router(); + router.use('/mountpath', proxy.createRequestHandler({ permissionApi })); + const app = express().use(router); + const requestPromise = request(app) .get('/mountpath/api/v1/namespaces') - .set(HEADER_KUBERNETES_CLUSTER, 'cluster1') - .set('Authorization', 'my-token'); + .set(HEADER_KUBERNETES_CLUSTER, 'cluster1'); worker.use(rest.all(requestPromise.url, (req: any) => req.passthrough())); @@ -212,7 +218,7 @@ describe('KubernetesProxy', () => { expect(response.status).toEqual(200); }); - it('should add a serviceAccountToken to the request headers if one isnt provided in request and one isnt set up in cluster details', async () => { + it('should add a authTranslator provided serviceAccountToken as authorization headers to kubeapi if one isnt provided in request and one isnt set up in cluster details', async () => { worker.use( rest.get('https://localhost:9999/api/v1/namespaces', (req, res, ctx) => { if (!req.headers.get('Authorization')) { @@ -253,10 +259,10 @@ describe('KubernetesProxy', () => { authProvider: 'googleServiceAccount', } as ClusterDetails); - const app = express().use( - '/mountpath', - proxy.createRequestHandler({ permissionApi }), - ); + const router = Router(); + router.use('/mountpath', proxy.createRequestHandler({ permissionApi })); + const app = express().use(router); + const requestPromise = request(app) .get('/mountpath/api/v1/namespaces') .set(HEADER_KUBERNETES_CLUSTER, 'cluster1'); @@ -314,10 +320,10 @@ describe('KubernetesProxy', () => { authProvider: 'googleServiceAccount', } as ClusterDetails); - const app = express().use( - '/mountpath', - proxy.createRequestHandler({ permissionApi }), - ); + const router = Router(); + router.use('/mountpath', proxy.createRequestHandler({ permissionApi })); + const app = express().use(router); + const requestPromise = request(app) .get('/mountpath/api/v1/namespaces') .set(HEADER_KUBERNETES_CLUSTER, 'cluster1') @@ -334,4 +340,210 @@ describe('KubernetesProxy', () => { items: [], }); }); + + it('should not invoke authTranslator if Backstage-Kubernetes-Authorization field is provided', async () => { + worker.use( + rest.get('https://localhost:9999/api/v1/namespaces', (req, res, ctx) => { + if (!req.headers.get('Authorization')) { + return res(ctx.status(401)); + } + + if (req.headers.get('Authorization') !== 'tokenB') { + return res(ctx.status(403)); + } + + return res( + ctx.status(200), + ctx.json({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }), + ); + }), + ); + + permissionApi.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); + + clusterSupplier.getClusters.mockResolvedValue([ + { + name: 'cluster1', + url: 'https://localhost:9999', + authProvider: 'googleServiceAccount', + }, + ] as ClusterDetails[]); + + const router = Router(); + router.use('/mountpath', proxy.createRequestHandler({ permissionApi })); + const app = express().use(router); + + const requestPromise = request(app) + .get('/mountpath/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'cluster1') + .set(HEADER_KUBERNETES_AUTH, 'tokenB'); + + worker.use(rest.all(requestPromise.url, (req: any) => req.passthrough())); + + const response = await requestPromise; + + expect(authTranslator.decorateClusterDetailsWithAuth).toHaveBeenCalledTimes( + 0, + ); + expect(response.status).toEqual(200); + expect(response.body).toStrictEqual({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }); + }); + + it('returns a response with a localKubectlProxy auth provider configuration', async () => { + proxy = new KubernetesProxy({ + logger: getVoidLogger(), + clusterSupplier: new LocalKubectlProxyClusterLocator(), + authTranslator: new NoopKubernetesAuthTranslator(), + }); + + worker.use( + rest.get('http://localhost:8001/api/v1/namespaces', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }), + ); + }), + ); + + permissionApi.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); + + const router = Router(); + router.use('/mountpath', proxy.createRequestHandler({ permissionApi })); + const app = express().use(router); + + const requestPromise = request(app) + .get('/mountpath/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'local'); + + worker.use(rest.all(requestPromise.url, (req: any) => req.passthrough())); + + const response = await requestPromise; + + expect(response.status).toEqual(200); + expect(response.body).toStrictEqual({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }); + }); + + it('should return a 400 error if Backstage-Kubernetes-Cluster field isnt provided in request', async () => { + worker.use( + rest.get('https://localhost:9999/api/v1/namespaces', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }), + ); + }), + ); + + permissionApi.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); + + clusterSupplier.getClusters.mockResolvedValue([ + { + name: 'cluster1', + url: 'https://localhost:9999', + authProvider: 'googleServiceAccount', + }, + ] as ClusterDetails[]); + + authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + name: 'cluster1', + url: 'https://localhost:9999', + serviceAccountToken: 'tokenA', + authProvider: 'googleServiceAccount', + } as ClusterDetails); + + const router = Router(); + router.use('/mountpath', proxy.createRequestHandler({ permissionApi })); + router.use(errorHandler()); + const app = express().use(router); + + const requestPromise = request(app) + .get('/mountpath/api/v1/namespaces') + .set(HEADER_KUBERNETES_AUTH, 'tokenB'); + + worker.use(rest.all(requestPromise.url, (req: any) => req.passthrough())); + + const response = await requestPromise; + + expect(response.status).toEqual(400); + }); + + it('returns a 500 error if authTranslator errors out and Backstage-Kubernetes-Authorization field is not provided', async () => { + worker.use( + rest.get('https://localhost:9999/api/v1/namespaces', (req, res, ctx) => { + if (!req.headers.get('Authorization')) { + return res(ctx.status(401)); + } + + if (req.headers.get('Authorization') !== 'tokenB') { + return res(ctx.status(403)); + } + + return res( + ctx.status(200), + ctx.json({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }), + ); + }), + ); + + permissionApi.authorize.mockResolvedValue([ + { result: AuthorizeResult.ALLOW }, + ]); + + clusterSupplier.getClusters.mockResolvedValue([ + { + name: 'cluster1', + url: 'https://localhost:9999', + authProvider: 'google', + serviceAccountToken: 'client-side-token', + }, + ] as ClusterDetails[]); + + authTranslator.decorateClusterDetailsWithAuth.mockRejectedValue( + Error('some internal error'), + ); + + const router = Router(); + router.use('/mountpath', proxy.createRequestHandler({ permissionApi })); + router.use(errorHandler()); + const app = express().use(router); + + const requestPromise = request(app) + .get('/mountpath/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'cluster1'); + + worker.use(rest.all(requestPromise.url, (req: any) => req.passthrough())); + + const response = await requestPromise; + + expect(response.status).toEqual(500); + }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index eb6748ffba..1a705feb84 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -98,26 +98,29 @@ export class KubernetesProxy { req.header('authorization'), ); - const authorizeResponse = ( - await permissionApi.authorize( - [{ permission: kubernetesProxyPermission }], - { - token, - }, - ) - )[0]; + const authorizeResponse = await permissionApi.authorize( + [{ permission: kubernetesProxyPermission }], + { + token, + }, + ); + const auth = authorizeResponse[0]; - if (authorizeResponse.result === AuthorizeResult.DENY) { + if (auth.result === AuthorizeResult.DENY) { res.status(403).json({ error: new NotAllowedError('Unauthorized') }); return; } - const cluster = await this.getClusterForRequest(req).then(cd => - this.authTranslator.decorateClusterDetailsWithAuth(cd, {}), - ); - if (!req.headers.authorization) { - req.headers.authorization = `Bearer ${cluster.serviceAccountToken}`; - } + req.headers.authorization = + req.header(HEADER_KUBERNETES_AUTH) ?? + `Bearer ${ + ( + await this.getClusterForRequest(req).then(cd => + this.authTranslator.decorateClusterDetailsWithAuth(cd, {}), + ) + ).serviceAccountToken + }`; + const middleware = await this.getMiddleware(req); middleware(req, res, next); }; @@ -163,13 +166,6 @@ export class KubernetesProxy { }; res.status(500).json(body); }, - onProxyReq: (proxyReq, req) => { - // the kubernetes proxy endpoint expects a header field labeled `Backstage-Kubernetes-Authorization` that will be used to authenticate with the Kubernetes Api. The token provided as a value should be an bearer token for the target cluster. - if (req.header(HEADER_KUBERNETES_AUTH)) { - const token = req.header(HEADER_KUBERNETES_AUTH) ?? ''; - proxyReq.setHeader('Authorization', token); - } - }, }); this.middlewareForClusterName.set(originalCluster.name, middleware); } diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 520b9ed3ee..9c8849c668 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-kubernetes-common +## 0.6.3-next.0 + +### Patch Changes + +- 05f1d74539d: AKS access tokens can now be sent over the wire to the Kubernetes backend. + ## 0.6.2 ### Patch Changes diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index 6c88074e50..56faeea7d9 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -226,6 +226,8 @@ export const kubernetesProxyPermission: BasicPermission; // @public (undocumented) export interface KubernetesRequestAuth { + // (undocumented) + aks?: string; // (undocumented) google?: string; // (undocumented) diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 7b0cdad9c0..80911bac00 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-common", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", - "version": "0.6.2", + "version": "0.6.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index f5701d49fe..8eabb16d24 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -35,6 +35,7 @@ import { Entity } from '@backstage/catalog-model'; /** @public */ export interface KubernetesRequestAuth { google?: string; + aks?: string; oidc?: { [key: string]: string; }; diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index a012240830..0842800552 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-kubernetes +## 0.9.0-next.2 + +### Patch Changes + +- c7bad1005ba: The Kubernetes plugin now requests AKS access tokens from Azure when retrieving + objects from clusters configured with `authProvider: aks` and sets `auth.aks` in + its request bodies appropriately. +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-kubernetes-common@0.6.3-next.0 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.9.0-next.1 ### Minor Changes diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 60f5d3fc99..5529b7910f 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -281,6 +281,7 @@ export const kubernetesApiRef: ApiRef; // @public (undocumented) export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { constructor(options: { + microsoftAuthApi: OAuthApi; googleAuthApi: OAuthApi; oidcProviders?: { [key: string]: OpenIdConnectApi; diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 75b51cdbb5..a581c1a82b 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.9.0-next.1", + "version": "0.9.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/AksKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/AksKubernetesAuthProvider.ts new file mode 100644 index 0000000000..401fc1e127 --- /dev/null +++ b/plugins/kubernetes/src/kubernetes-auth-provider/AksKubernetesAuthProvider.ts @@ -0,0 +1,39 @@ +/* + * 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 { OAuthApi } from '@backstage/core-plugin-api'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { KubernetesAuthProvider } from './types'; + +export class AksKubernetesAuthProvider implements KubernetesAuthProvider { + constructor(private readonly microsoftAuthApi: OAuthApi) {} + + async decorateRequestBodyForAuth( + requestBody: KubernetesRequestBody, + ): Promise { + return { + ...requestBody, + auth: { ...requestBody.auth, aks: (await this.getCredentials()).token }, + }; + } + + async getCredentials(): Promise<{ token?: string }> { + return { + token: await this.microsoftAuthApi.getAccessToken( + '6dae42f8-4368-4678-94ff-3960e28e3630/user.read', + ), + }; + } +} diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts index a83cbcfaa4..14e574022a 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts @@ -39,11 +39,18 @@ const requestBody: KubernetesRequestBody = { }; describe('KubernetesAuthProviders tests', () => { - const kap = new KubernetesAuthProviders({ - googleAuthApi: new MockAuthApi('googleToken'), - oidcProviders: { - okta: new MockAuthApi('oktaToken'), - }, + let microsoftAuthApi: MockAuthApi; + let kap: KubernetesAuthProviders; + + beforeEach(() => { + microsoftAuthApi = new MockAuthApi('aksToken'); + kap = new KubernetesAuthProviders({ + microsoftAuthApi, + googleAuthApi: new MockAuthApi('googleToken'), + oidcProviders: { + okta: new MockAuthApi('oktaToken'), + }, + }); }); it('adds token to request body for google authProvider', async () => { @@ -52,6 +59,15 @@ describe('KubernetesAuthProviders tests', () => { expect(details.auth?.google).toBe('googleToken'); }); + it('adds token to request body for aks authProvider', async () => { + const details = await kap.decorateRequestBodyForAuth('aks', requestBody); + + expect(details.auth?.aks).toBe('aksToken'); + expect(microsoftAuthApi.getAccessToken).toHaveBeenCalledWith( + '6dae42f8-4368-4678-94ff-3960e28e3630/user.read', + ); + }); + it('adds token to request body for oidc authProvider', async () => { const details = await kap.decorateRequestBodyForAuth( 'oidc.okta', diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts index 836641f1ed..84154d5e96 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts @@ -20,6 +20,7 @@ import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider'; import { ServerSideKubernetesAuthProvider } from './ServerSideAuthProvider'; import { OAuthApi, OpenIdConnectApi } from '@backstage/core-plugin-api'; import { OidcKubernetesAuthProvider } from './OidcKubernetesAuthProvider'; +import { AksKubernetesAuthProvider } from './AksKubernetesAuthProvider'; export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { private readonly kubernetesAuthProviderMap: Map< @@ -28,6 +29,7 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { >; constructor(options: { + microsoftAuthApi: OAuthApi; googleAuthApi: OAuthApi; oidcProviders?: { [key: string]: OpenIdConnectApi }; }) { @@ -56,6 +58,10 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { 'localKubectlProxy', new ServerSideKubernetesAuthProvider(), ); + this.kubernetesAuthProviderMap.set( + 'aks', + new AksKubernetesAuthProvider(options.microsoftAuthApi), + ); if (options.oidcProviders) { Object.keys(options.oidcProviders).forEach(provider => { diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index e10b4856b7..02c7bfd2d6 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -87,7 +87,11 @@ export const kubernetesPlugin = createPlugin({ onelogin: oneloginAuthApi, }; - return new KubernetesAuthProviders({ googleAuthApi, oidcProviders }); + return new KubernetesAuthProviders({ + microsoftAuthApi, + googleAuthApi, + oidcProviders, + }); }, }), ], diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 67aa76b0f9..a90222b49a 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-lighthouse +## 0.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.4.3-next.1 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 7afa358f76..7e66e6fdc6 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.4.3-next.1", + "version": "0.4.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/linguist/CHANGELOG.md b/plugins/linguist/CHANGELOG.md index 7c3a38f09d..5c12d0df14 100644 --- a/plugins/linguist/CHANGELOG.md +++ b/plugins/linguist/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-linguist +## 0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.3-next.1 ### Patch Changes diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json index e71271c03f..885462b0e0 100644 --- a/plugins/linguist/package.json +++ b/plugins/linguist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-linguist", - "version": "0.1.3-next.1", + "version": "0.1.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/microsoft-calendar/CHANGELOG.md b/plugins/microsoft-calendar/CHANGELOG.md index 6e4ca92146..98e2cbf1c1 100644 --- a/plugins/microsoft-calendar/CHANGELOG.md +++ b/plugins/microsoft-calendar/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-microsoft-calendar +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.3-next.0 ### Patch Changes diff --git a/plugins/microsoft-calendar/package.json b/plugins/microsoft-calendar/package.json index a28d9a5111..ca444bf7df 100644 --- a/plugins/microsoft-calendar/package.json +++ b/plugins/microsoft-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-microsoft-calendar", - "version": "0.1.3-next.0", + "version": "0.1.3-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md index 6e3142ef09..1bbac565ca 100644 --- a/plugins/newrelic-dashboard/CHANGELOG.md +++ b/plugins/newrelic-dashboard/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-newrelic-dashboard +## 0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.11-next.1 ### Patch Changes diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index c7d6ee5733..696762913b 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic-dashboard", - "version": "0.2.11-next.1", + "version": "0.2.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index b9beb277dc..88daca0ff0 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-newrelic +## 0.3.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.36-next.0 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 117d51f9e9..628280099c 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-newrelic", "description": "A Backstage plugin that integrates towards New Relic", - "version": "0.3.36-next.0", + "version": "0.3.36-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/octopus-deploy/CHANGELOG.md b/plugins/octopus-deploy/CHANGELOG.md index 98c05807bf..228c8b5a18 100644 --- a/plugins/octopus-deploy/CHANGELOG.md +++ b/plugins/octopus-deploy/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-octopus-deploy +## 0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.0-next.1 ### Minor Changes diff --git a/plugins/octopus-deploy/package.json b/plugins/octopus-deploy/package.json index f8cfe04450..2140f130e5 100644 --- a/plugins/octopus-deploy/package.json +++ b/plugins/octopus-deploy/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-octopus-deploy", - "version": "0.2.0-next.1", + "version": "0.2.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index 54b536cb79..d7a5b72817 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-org-react +## 0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.7-next.1 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index 7778af38db..5c33388f69 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.7-next.1", + "version": "0.1.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 017d0c6db6..f729d46e19 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-org +## 0.6.8-next.2 + +### Patch Changes + +- 6e387c077a4: Changed the MembersListCard component to allow displaying aggregated members when viewing a group. Now, a toggle switch can be displayed that lets you switch between showing direct members and aggregated members. + + To enable this new feature, set the showAggregateMembersToggle prop on EntityMembersListCard: + + ```jsx + // In packages/app/src/components/catalog/EntityPage.tsx + const groupPage = ( + // ... + + // ... + ); + ``` + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.6.8-next.1 ### Patch Changes diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md index 1111253119..7f21bfbb6b 100644 --- a/plugins/org/api-report.md +++ b/plugins/org/api-report.md @@ -20,6 +20,7 @@ export const EntityGroupProfileCard: (props: { export const EntityMembersListCard: (props: { memberDisplayTitle?: string | undefined; pageSize?: number | undefined; + showAggregateMembersToggle?: boolean | undefined; }) => JSX.Element; // @public (undocumented) @@ -47,6 +48,7 @@ export const GroupProfileCard: (props: { export const MembersListCard: (props: { memberDisplayTitle?: string; pageSize?: number; + showAggregateMembersToggle?: boolean; }) => JSX.Element; // @public diff --git a/plugins/org/package.json b/plugins/org/package.json index 8d6490b2e2..6755d8fd64 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.6.8-next.1", + "version": "0.6.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,10 @@ "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", + "@backstage/plugin-catalog": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", "@backstage/test-utils": "workspace:^", + "@backstage/types": "workspace:^", "@testing-library/dom": "^8.0.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", diff --git a/plugins/org/src/__testUtils__/catalogMocks.ts b/plugins/org/src/__testUtils__/catalogMocks.ts new file mode 100644 index 0000000000..b14856e345 --- /dev/null +++ b/plugins/org/src/__testUtils__/catalogMocks.ts @@ -0,0 +1,340 @@ +/* + * 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 { + CatalogApi, + GetEntitiesByRefsRequest, + GetEntitiesRequest, +} from '@backstage/catalog-client'; +import { + CompoundEntityRef, + Entity, + GroupEntity, + stringifyEntityRef, +} from '@backstage/catalog-model'; + +export const groupA: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'group-a', + title: 'Group A', + description: 'Testing Group A', + }, + spec: { + type: 'testing-group', + children: [], + }, + relations: [ + { + type: 'parentOf', + targetRef: 'group:default/group-b', + }, + { + type: 'parentOf', + targetRef: 'group:default/group-c', + }, + ], +}; + +export const groupAWithALeader: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'group-a', + title: 'Group A', + description: 'Testing Group A', + }, + spec: { + type: 'testing-group', + children: [], + }, + relations: [ + { + type: 'teamLeadBy', + targetRef: 'user:default/group-a-user-one', + }, + { + type: 'parentOf', + targetRef: 'group:default/group-b', + }, + { + type: 'parentOf', + targetRef: 'group:default/group-c', + }, + ], +}; + +export const groupARef: CompoundEntityRef = { + kind: 'group', + name: 'group-a', + namespace: 'default', +}; + +export const groupB: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'group-b', + title: 'Group B', + description: 'Testing Group B', + }, + spec: { + type: 'testing-group', + children: [], + }, + relations: [ + { + type: 'parentOf', + targetRef: 'group:default/group-a', + }, + { + type: 'parentOf', + targetRef: 'group:default/group-d', + }, + ], +}; + +export const groupBRef: CompoundEntityRef = { + kind: 'group', + name: 'group-b', + namespace: 'default', +}; + +export const groupC: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'group-c', + title: 'Group C', + description: 'Testing Group C', + }, + spec: { + type: 'testing-group', + children: [], + }, +}; + +export const groupCRef: CompoundEntityRef = { + kind: 'group', + name: 'group-c', + namespace: 'default', +}; + +export const groupD: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'group-d', + title: 'Group D', + description: 'Testing Group D', + }, + spec: { + type: 'testing-group', + children: [], + }, + relations: [ + { + type: 'parentOf', + targetRef: 'group:default/group-c', + }, + { + type: 'parentOf', + targetRef: 'group:default/group-e', + }, + ], +}; + +export const groupDRef: CompoundEntityRef = { + kind: 'group', + name: 'group-d', + namespace: 'default', +}; + +export const groupE: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'group-e', + title: 'Group E', + description: 'Testing Group E', + }, + spec: { + type: 'testing-group', + children: [], + }, +}; + +export const groupERef: CompoundEntityRef = { + kind: 'group', + name: 'group-e', + namespace: 'default', +}; + +export const groupF: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'group-f', + title: 'Group F', + description: 'Testing Group F', + }, + spec: { + type: 'testing-group', + children: [], + }, +}; + +export const groupAUserOne: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'group-a-user-one', + }, + spec: { + profile: { + displayName: 'Group A User One', + email: 'group-a-user-one@testing.email', + picture: + 'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg', + }, + }, +}; + +export const groupBUserOne: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'group-b-user-one', + }, + spec: { + profile: { + displayName: 'Group B User One', + email: 'group-b-user-one@testing.email', + picture: + 'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg', + }, + }, +}; + +export const groupDUserOne: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'group-d-user-one', + }, + spec: { + profile: { + displayName: 'Group D User One', + email: 'group-d-user-one@testing.email', + picture: + 'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg', + }, + }, +}; + +export const groupEUserOne: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'group-e-user-one', + }, + spec: { + profile: { + displayName: 'Group E User One', + email: 'group-e-user-one@testing.email', + picture: + 'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg', + }, + }, +}; + +export const groupFUserOne: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'group-f-user-one', + }, + spec: { + profile: { + displayName: 'Group F User One', + email: 'group-f-user-one@testing.email', + picture: + 'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg', + }, + }, +}; + +export const duplicatedUser: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'duplicated-user', + }, + spec: { + profile: { + displayName: 'Duplicated User', + email: 'duplicated-user@testing.email', + picture: + 'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg', + }, + }, +}; + +const mockedRefsToRelationsMap = new Map([ + ['group:default/group-a', groupA], + ['group:default/group-b', groupB], + ['group:default/group-c', groupC], + ['group:default/group-d', groupD], + ['group:default/group-e', groupE], + ['group:default/group-f', groupF], +]); + +const mockedMembersMapping = new Map([ + ['group:default/group-a', [groupAUserOne, duplicatedUser]], + ['group:default/group-b', [groupBUserOne]], + ['group:default/group-c', []], + ['group:default/group-d', [groupDUserOne, duplicatedUser]], + ['group:default/group-e', [groupEUserOne, duplicatedUser]], + ['group:default/group-f', [groupFUserOne]], +]); + +type Nullable = T | undefined; + +export const mockedCatalogApiSupportingGroups: Partial = { + getEntities: async (request?: GetEntitiesRequest) => { + const actualFilter = (request?.filter as Nullable<{ + 'relations.memberof': string[]; + }>) ?? { 'relations.memberof': [] }; + const items = + actualFilter['relations.memberof'].length > 0 + ? (actualFilter['relations.memberof'] + .map(group => mockedMembersMapping.get(group)) + .flat() + .filter(entity => !!entity) as Entity[]) + : [...mockedMembersMapping.values()].flat(); + + return { items }; + }, + getEntityByRef: async (entityRef: CompoundEntityRef) => + mockedRefsToRelationsMap.get(stringifyEntityRef(entityRef)), + getEntitiesByRefs: async (request: GetEntitiesByRefsRequest) => { + const items = request.entityRefs.map(entityRef => + mockedRefsToRelationsMap.get(entityRef), + ); + return { items }; + }, +}; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx index 073f892302..6b34d8b934 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx @@ -20,6 +20,10 @@ import { TestApiProvider } from '@backstage/test-utils'; import { Grid } from '@material-ui/core'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { + groupA, + mockedCatalogApiSupportingGroups, +} from '../../../../__testUtils__/catalogMocks'; import { MembersListCard } from './MembersListCard'; export default { @@ -132,3 +136,17 @@ export const Empty = () => ( ); + +export const AggregateMembersToggle = () => ( + + + + + + + + + + + +); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index cc524b60a2..47e954eb17 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -19,14 +19,26 @@ import { CatalogApi, catalogApiRef, EntityProvider, + StarredEntitiesApi, + starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; import { + renderInTestApp, renderWithEffects, TestApiProvider, wrapInTestApp, } from '@backstage/test-utils'; import React from 'react'; import { MembersListCard } from './MembersListCard'; +import { + groupA, + mockedCatalogApiSupportingGroups, +} from '../../../../__testUtils__/catalogMocks'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; +import { EntityLayout } from '@backstage/plugin-catalog'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Observable } from '@backstage/types'; // Mock needed because jsdom doesn't correctly implement box-sizing // https://github.com/ShinyChang/React-Text-Truncate/issues/70 @@ -41,6 +53,20 @@ jest.mock('react-text-truncate', () => { }; }); +const mockedStarredEntitiesApi: Partial = { + starredEntitie$: () => { + return { + subscribe: () => { + return { + unsubscribe() { + // This is intentional + }, + }; + }, + } as Observable>; + }, +}; + describe('MemberTab Test', () => { const groupEntity: GroupEntity = { apiVersion: 'backstage.io/v1alpha1', @@ -131,4 +157,164 @@ describe('MemberTab Test', () => { expect(rendered.getByText('Testers (1)')).toBeInTheDocument(); }); + + describe('Aggregate members toggle', () => { + it('Does not show the aggregate members toggle if the showAggregateMembersToggle prop is undefined', async () => { + await renderInTestApp( + + + + + + + + + , + ); + + const toggleSwitch = screen.queryByRole('checkbox'); + expect(toggleSwitch).toBeNull(); + }); + + it('Shows the aggregate members toggle if the showAggregateMembersToggle prop is true', async () => { + await renderInTestApp( + + + + + + + + + , + ); + + expect(screen.queryByRole('checkbox')).toBeInTheDocument(); + }); + + it('Shows only direct members if the showAggregateMembersToggle prop is undefined', async () => { + await renderInTestApp( + + + + + + + + + , + ); + + const displayedMemberNames = screen.queryAllByTestId('user-link'); + const duplicatedUserText = screen.getByText('Duplicated User'); + const groupAUserOneText = screen.getByText('Group A User One'); + + expect(displayedMemberNames).toHaveLength(2); + expect(duplicatedUserText).toBeInTheDocument(); + expect(groupAUserOneText).toBeInTheDocument(); + expect( + duplicatedUserText.compareDocumentPosition(groupAUserOneText), + ).toBe(Node.DOCUMENT_POSITION_FOLLOWING); + }); + + it('Shows only direct members if the aggregate members switch is turned off', async () => { + await renderInTestApp( + + + + + + + + + , + ); + + const displayedMemberNames = screen.queryAllByTestId('user-link'); + const duplicatedUserText = screen.getByText('Duplicated User'); + const groupAUserOneText = screen.getByText('Group A User One'); + + expect(displayedMemberNames).toHaveLength(2); + expect(duplicatedUserText).toBeInTheDocument(); + expect(groupAUserOneText).toBeInTheDocument(); + expect( + duplicatedUserText.compareDocumentPosition(groupAUserOneText), + ).toBe(Node.DOCUMENT_POSITION_FOLLOWING); + }); + + it('Shows all descendant members of the group when the aggregate users switch is turned on, showing duplicated members only once', async () => { + await renderInTestApp( + + + + + + + + + , + ); + + // Click the toggle switch + await userEvent.click(screen.getByRole('checkbox')); + + const displayedMemberNames = screen.queryAllByTestId('user-link'); + const duplicatedUserText = screen.getByText('Duplicated User'); + const groupAUserOneText = screen.getByText('Group A User One'); + const groupBUserOneText = screen.getByText('Group B User One'); + const groupDUserOneText = screen.getByText('Group D User One'); + const groupEUserOneText = screen.getByText('Group E User One'); + + expect(displayedMemberNames).toHaveLength(5); + + expect(duplicatedUserText).toBeInTheDocument(); + expect(groupAUserOneText).toBeInTheDocument(); + expect(groupBUserOneText).toBeInTheDocument(); + expect(groupDUserOneText).toBeInTheDocument(); + expect(groupEUserOneText).toBeInTheDocument(); + + expect( + duplicatedUserText.compareDocumentPosition(groupAUserOneText), + ).toBe(Node.DOCUMENT_POSITION_FOLLOWING); + expect(groupAUserOneText.compareDocumentPosition(groupBUserOneText)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + expect(groupBUserOneText.compareDocumentPosition(groupDUserOneText)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + expect(groupDUserOneText.compareDocumentPosition(groupEUserOneText)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + }); + }); }); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index eb61989144..0a5386ea20 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -30,11 +30,12 @@ import { createStyles, Grid, makeStyles, + Switch, Theme, Typography, } from '@material-ui/core'; import Pagination from '@material-ui/lab/Pagination'; -import React from 'react'; +import React, { useState } from 'react'; import { generatePath } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; @@ -47,6 +48,10 @@ import { OverflowTooltip, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +import { + getAllDesendantMembersForGroupEntity, + removeDuplicateEntitiesFrom, +} from '../../../../helpers/helpers'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -98,6 +103,7 @@ const MemberComponent = (props: { member: UserEntity }) => { > { export const MembersListCard = (props: { memberDisplayTitle?: string; pageSize?: number; + showAggregateMembersToggle?: boolean; }) => { - const { memberDisplayTitle = 'Members', pageSize = 50 } = props; + const { + memberDisplayTitle = 'Members', + pageSize = 50, + showAggregateMembersToggle, + } = props; const { entity: groupEntity } = useEntity(); const { @@ -144,10 +155,23 @@ export const MembersListCard = (props: { setPage(pageIndex); }; + const [showAggregateMembers, setShowAggregateMembers] = useState(false); + + const { loading: loadingDescendantMembers, value: descendantMembers } = + useAsync(async () => { + if (!showAggregateMembersToggle) { + return [] as UserEntity[]; + } + + return await getAllDesendantMembersForGroupEntity( + groupEntity, + catalogApi, + ); + }, [catalogApi, groupEntity, showAggregateMembersToggle]); const { loading, error, - value: members, + value: directMembers, } = useAsync(async () => { const membersList = await catalogApi.getEntities({ filter: { @@ -165,6 +189,15 @@ export const MembersListCard = (props: { return membersList.items as UserEntity[]; }, [catalogApi, groupEntity]); + const members = removeDuplicateEntitiesFrom( + [ + ...(directMembers ?? []), + ...(descendantMembers && showAggregateMembers ? descendantMembers : []), + ].sort((a, b) => + stringifyEntityRef(a).localeCompare(stringifyEntityRef(b)), + ), + ) as UserEntity[]; + if (loading) { return ; } else if (error) { @@ -193,21 +226,39 @@ export const MembersListCard = (props: { subheader={`of ${displayName}`} {...(nbPages <= 1 ? {} : { actions: pagination })} > - - {members && members.length > 0 ? ( - members - .slice(pageSize * (page - 1), pageSize * page) - .map(member => ( - - )) - ) : ( - - - This group has no {memberDisplayTitle.toLocaleLowerCase()}. - - - )} - + {showAggregateMembersToggle && ( + <> + Direct Members + { + setShowAggregateMembers(!showAggregateMembers); + }} + inputProps={{ 'aria-label': 'Users Type Switch' }} + /> + Aggregated Members + + )} + {showAggregateMembers && loadingDescendantMembers ? ( + + ) : ( + + {members && members.length > 0 ? ( + members + .slice(pageSize * (page - 1), pageSize * page) + .map(member => ( + + )) + ) : ( + + + This group has no {memberDisplayTitle.toLocaleLowerCase()}. + + + )} + + )} ); diff --git a/plugins/org/src/helpers/helpers.test.ts b/plugins/org/src/helpers/helpers.test.ts new file mode 100644 index 0000000000..4cee5422ce --- /dev/null +++ b/plugins/org/src/helpers/helpers.test.ts @@ -0,0 +1,135 @@ +/* + * 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 { CatalogApi } from '@backstage/catalog-client'; +import { + getAllDesendantMembersForGroupEntity, + getMembersFromGroups, + getDescendantGroupsFromGroup, + removeDuplicateEntitiesFrom, +} from './helpers'; +import { + groupA, + groupARef, + groupB, + groupBRef, + groupC, + groupCRef, + groupD, + groupDRef, + groupE, + groupERef, + groupAUserOne, + groupBUserOne, + groupDUserOne, + groupEUserOne, + duplicatedUser, + mockedCatalogApiSupportingGroups, +} from '../__testUtils__/catalogMocks'; + +describe('Helper functions', () => { + it('getAllDesendantMembersForGroupEntity correctly recursively returns all descendant members', async () => { + const catalogApi = mockedCatalogApiSupportingGroups as CatalogApi; + + const actualGroupADescendantMembers = + await getAllDesendantMembersForGroupEntity(groupA, catalogApi); + const actualGroupCDescendantMembers = + await getAllDesendantMembersForGroupEntity(groupC, catalogApi); + + expect(actualGroupADescendantMembers).toStrictEqual([ + groupBUserOne, + groupDUserOne, + duplicatedUser, + groupEUserOne, + duplicatedUser, + ]); + expect(actualGroupCDescendantMembers).toStrictEqual([]); + }); + + it('getMembersFromGroups correctly returns all members of provided groups', async () => { + const catalogApi = mockedCatalogApiSupportingGroups as CatalogApi; + + const actualNoGroupsMembers = await getMembersFromGroups([], catalogApi); + const actualGroupAMembers = await getMembersFromGroups( + [groupARef], + catalogApi, + ); + const actualGroupCMembers = await getMembersFromGroups( + [groupCRef], + catalogApi, + ); + const actualMultipleGroupMembers = await getMembersFromGroups( + [groupARef, groupBRef, groupCRef], + catalogApi, + ); + + expect(actualNoGroupsMembers).toStrictEqual([]); + expect(actualGroupAMembers).toStrictEqual([groupAUserOne, duplicatedUser]); + expect(actualGroupCMembers).toStrictEqual([]); + expect(actualMultipleGroupMembers).toStrictEqual([ + groupAUserOne, + duplicatedUser, + groupBUserOne, + ]); + }); + + it('removeDuplicateEntitiesFrom correctly removes duplicate entities', () => { + const actualNoDuplicatedEntitiesResult = removeDuplicateEntitiesFrom([ + groupA, + groupB, + groupC, + groupD, + ]); + const actualDuplicatedEntitiesResult = removeDuplicateEntitiesFrom([ + groupA, + groupB, + groupB, + groupC, + groupC, + groupD, + groupE, + ]); + const actualNoEntitiesProvidedResult = removeDuplicateEntitiesFrom([]); + + expect(actualNoDuplicatedEntitiesResult).toStrictEqual([ + groupA, + groupB, + groupC, + groupD, + ]); + expect(actualDuplicatedEntitiesResult).toStrictEqual([ + groupA, + groupB, + groupC, + groupD, + groupE, + ]); + expect(actualNoEntitiesProvidedResult).toStrictEqual([]); + }); + + it('getDescendantGroupsFromGroup correctly recursively returns descendant groups, ignoring duplicates', async () => { + const actualDescendantGroups = await getDescendantGroupsFromGroup( + groupA, + mockedCatalogApiSupportingGroups as CatalogApi, + ); + expect(actualDescendantGroups).toStrictEqual([ + groupBRef, + groupCRef, + groupDRef, + groupERef, + ]); + }); +}); diff --git a/plugins/org/src/helpers/helpers.ts b/plugins/org/src/helpers/helpers.ts new file mode 100644 index 0000000000..7da4ee32d8 --- /dev/null +++ b/plugins/org/src/helpers/helpers.ts @@ -0,0 +1,118 @@ +/* + * 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 { + CompoundEntityRef, + DEFAULT_NAMESPACE, + Entity, + GroupEntity, + RELATION_PARENT_OF, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; +import { + CatalogApi, + getEntityRelations, +} from '@backstage/plugin-catalog-react'; + +export const getMembersFromGroups = async ( + groups: CompoundEntityRef[], + catalogApi: CatalogApi, +) => { + const membersList = + groups.length === 0 + ? { items: [] } + : await catalogApi.getEntities({ + filter: { + kind: 'User', + 'relations.memberof': groups.map(group => + stringifyEntityRef({ + kind: 'group', + namespace: group.namespace.toLocaleLowerCase('en-US'), + name: group.name.toLocaleLowerCase('en-US'), + }), + ), + }, + }); + + return membersList.items as UserEntity[]; +}; + +export const getDescendantGroupsFromGroup = async ( + group: GroupEntity, + catalogApi: CatalogApi, +) => { + const alreadyQueuedOrExpandedGroupNames = new Map(); + const groupRef: CompoundEntityRef = { + kind: group.kind, + namespace: group.metadata.namespace ?? DEFAULT_NAMESPACE, + name: group.metadata.name, + }; + + const groupQueue = [groupRef]; + const resultantGroupRefs: CompoundEntityRef[] = []; + + // Continue expanding groups until there are no more + while (groupQueue.length > 0) { + const activeGroupRef = groupQueue.shift() as CompoundEntityRef; + const activeGroup = await catalogApi.getEntityByRef(activeGroupRef); + alreadyQueuedOrExpandedGroupNames.set( + stringifyEntityRef(activeGroupRef), + true, + ); + + const childGroups = getEntityRelations(activeGroup, RELATION_PARENT_OF, { + kind: 'Group', + }).filter( + currentGroup => + !alreadyQueuedOrExpandedGroupNames.has( + stringifyEntityRef(currentGroup), + ), + ); + childGroups.forEach(childGroup => + alreadyQueuedOrExpandedGroupNames.set( + stringifyEntityRef(childGroup), + true, + ), + ); + + groupQueue.push(...childGroups); + resultantGroupRefs.push(...childGroups); + } + + return resultantGroupRefs; +}; + +export const getAllDesendantMembersForGroupEntity = async ( + groupEntity: GroupEntity, + catalogApi: CatalogApi, +) => + getMembersFromGroups( + await getDescendantGroupsFromGroup(groupEntity, catalogApi), + catalogApi, + ); + +export const removeDuplicateEntitiesFrom = (entityArray: Entity[]) => { + const seenEntities = new Map(); + + return entityArray.filter(entity => { + const stringifiedEntity = stringifyEntityRef(entity); + const isDuplicate = seenEntities.has(stringifiedEntity); + + seenEntities.set(stringifiedEntity, true); + return !isDuplicate; + }); +}; diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index c1ce6836eb..36f1e4a7ca 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-pagerduty +## 0.5.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.5.11-next.1 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 8e599ca240..89bc2367da 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.5.11-next.1", + "version": "0.5.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/periskop/CHANGELOG.md b/plugins/periskop/CHANGELOG.md index f205e47356..79f636a76a 100644 --- a/plugins/periskop/CHANGELOG.md +++ b/plugins/periskop/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-periskop +## 0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.16-next.1 ### Patch Changes diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index 97b7c26418..5b277bbcce 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop", - "version": "0.1.16-next.1", + "version": "0.1.16-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/playlist/CHANGELOG.md b/plugins/playlist/CHANGELOG.md index 363c92681d..30f6ef919d 100644 --- a/plugins/playlist/CHANGELOG.md +++ b/plugins/playlist/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-playlist +## 0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/playlist/package.json b/plugins/playlist/package.json index c542e6f05b..d30f4851d7 100644 --- a/plugins/playlist/package.json +++ b/plugins/playlist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/puppetdb/CHANGELOG.md b/plugins/puppetdb/CHANGELOG.md index f5bad45030..8581c7650b 100644 --- a/plugins/puppetdb/CHANGELOG.md +++ b/plugins/puppetdb/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-puppetdb +## 0.1.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.1-next.1 ### Patch Changes diff --git a/plugins/puppetdb/package.json b/plugins/puppetdb/package.json index 8e7cafac21..9782269666 100644 --- a/plugins/puppetdb/package.json +++ b/plugins/puppetdb/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-puppetdb", "description": "Backstage plugin to visualize resource information and Puppet facts from PuppetDB.", - "version": "0.1.1-next.1", + "version": "0.1.1-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 758244bd93..d4d34fec32 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-rollbar +## 0.4.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.4.18-next.1 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index b057062bb7..cd3c302b03 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.4.18-next.1", + "version": "0.4.18-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index f384a38e48..5ef00b8f91 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.13.2-next.2 + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + ## 0.1.2-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index bdc9238d2e..57602c2d27 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", - "version": "0.1.2-next.1", + "version": "0.1.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.ts index 58f84a8747..c741f6f6c0 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.ts @@ -88,7 +88,7 @@ export const getAndWriteAttachments = async ( }, }); if (!res.ok) { - throw ResponseError.fromResponse(res); + throw await ResponseError.fromResponse(res); } else if (res.body !== null) { fs.openSync(`${workspace}/${mkdocsDir}docs/img/${downloadTitle}`, 'w'); const writeStream = fs.createWriteStream( diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 05d8456582..1c1d336fd1 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.13.2-next.2 + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + ## 0.2.21-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index f8e494ae8b..d8649918c0 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.2.21-next.1", + "version": "0.2.21-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index 4d0b7edcc4..72999216df 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.2.0-next.2 + +### Minor Changes + +- 439e2986be1: Add a new scaffolder action for gitlab to ensure a group exists + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + ## 0.1.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/README.md b/plugins/scaffolder-backend-module-gitlab/README.md index 35353cf9c1..d071ca0cf6 100644 --- a/plugins/scaffolder-backend-module-gitlab/README.md +++ b/plugins/scaffolder-backend-module-gitlab/README.md @@ -45,6 +45,9 @@ const actions = [ createGitlabProjectDeployTokenAction({ integrations: integrations, }), + createGitlabGroupEnsureExistsAction({ + integrations: integrations, + }), ]; // Create Scaffolder Router @@ -104,13 +107,22 @@ spec: url: https://github.com/TEMPLATE values: name: ${{ parameters.name }} + - id: createGitlabGroup + name: Ensure Gitlab group exists + action: gitlab:group:ensureExists + input: + repoUrl: ${{ parameters.repoUrl }} + path: + - path + - to + - group - id: publish name: Publish action: publish:gitlab input: description: This is ${{ parameters.name }} - repoUrl: ${{ parameters.repoUrl }} + repoUrl: ${{ parameters.repoUrl }}?owner=${{ steps.createGitlabGroup.output.groupId }} sourcePath: pimcore defaultBranch: main diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md index 2ec05e74de..f7cf6024ec 100644 --- a/plugins/scaffolder-backend-module-gitlab/api-report.md +++ b/plugins/scaffolder-backend-module-gitlab/api-report.md @@ -7,19 +7,35 @@ import { JsonObject } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +// @public +export const createGitlabGroupEnsureExistsAction: (options: { + integrations: ScmIntegrationRegistry; +}) => TemplateAction< + { + path: string[]; + repoUrl: string; + token?: string | undefined; + }, + { + groupId?: number | undefined; + } +>; + // @public export const createGitlabProjectAccessTokenAction: (options: { integrations: ScmIntegrationRegistry; }) => TemplateAction< { - repoUrl: string; projectId: string | number; - name: string; - accessLevel: number; - scopes: string[]; + repoUrl: string; token?: string | undefined; + name?: string | undefined; + accessLevel?: number | undefined; + scopes?: string[] | undefined; }, - JsonObject + { + access_token: string; + } >; // @public @@ -27,14 +43,17 @@ export const createGitlabProjectDeployTokenAction: (options: { integrations: ScmIntegrationRegistry; }) => TemplateAction< { - repoUrl: string; - projectId: string | number; name: string; - username: string; - scopes: string[]; + projectId: string | number; + repoUrl: string; token?: string | undefined; + username?: string | undefined; + scopes?: string[] | undefined; }, - JsonObject + { + user: string; + deploy_token: string; + } >; // @public @@ -42,16 +61,16 @@ export const createGitlabProjectVariableAction: (options: { integrations: ScmIntegrationRegistry; }) => TemplateAction< { - repoUrl: string; - projectId: string | number; key: string; value: string; + projectId: string | number; + repoUrl: string; variableType: string; - variableProtected: boolean; - masked: boolean; - raw: boolean; - environmentScope: string; token?: string | undefined; + variableProtected?: boolean | undefined; + masked?: boolean | undefined; + raw?: boolean | undefined; + environmentScope?: string | undefined; }, JsonObject >; diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index ee10ecaf65..9c36616eb8 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitlab", - "version": "0.1.1-next.1", + "version": "0.2.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,8 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "@gitbeaker/node": "^35.8.0" + "@gitbeaker/node": "^35.8.0", + "zod": "^3.21.4" }, "devDependencies": { "@backstage/backend-common": "workspace:^", diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts new file mode 100644 index 0000000000..6fb119413f --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import { createGitlabGroupEnsureExistsAction } from './createGitlabGroupEnsureExistsAction'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; + +const mockGitlabClient = { + Groups: { + search: jest.fn(), + create: jest.fn(), + }, +}; +jest.mock('@gitbeaker/node', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:group:ensureExists', () => { + const mockContext = { + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should create a new group if it does not exists', async () => { + mockGitlabClient.Groups.search.mockResolvedValue([ + { + id: 1, + full_path: 'repos/bar', + }, + { + id: 2, + full_path: 'repos/foo', + }, + ]); + + mockGitlabClient.Groups.create.mockResolvedValue({ + id: 3, + full_path: 'repos/foo/bar', + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabGroupEnsureExistsAction({ integrations }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com', + path: ['foo', 'bar'], + }, + }); + + expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith('bar', 'bar', { + parent_id: 2, + }); + + expect(mockContext.output).toHaveBeenCalledWith('groupId', 3); + }); + + it('should return existing group if it does exists', async () => { + mockGitlabClient.Groups.search.mockResolvedValue([ + { + id: 1, + full_path: 'repos/bar', + }, + { + id: 2, + full_path: 'repos/foo', + }, + { + id: 42, + full_path: 'repos/foo/bar', + }, + ]); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabGroupEnsureExistsAction({ integrations }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com', + path: ['foo', 'bar'], + }, + }); + + expect(mockGitlabClient.Groups.create).not.toHaveBeenCalled(); + + expect(mockContext.output).toHaveBeenCalledWith('groupId', 42); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts new file mode 100644 index 0000000000..f96d57e356 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { Gitlab } from '@gitbeaker/node'; +import { GroupSchema } from '@gitbeaker/core/dist/types/resources/Groups'; +import commonGitlabConfig from '../commonGitlabConfig'; +import { getToken } from '../util'; +import { z } from 'zod'; + +/** + * Creates an `gitlab:group:ensureExists` Scaffolder action. + * + * @public + */ +export const createGitlabGroupEnsureExistsAction = (options: { + integrations: ScmIntegrationRegistry; +}) => { + const { integrations } = options; + + return createTemplateAction({ + id: 'gitlab:group:ensureExists', + description: 'Ensures a Gitlab group exists', + schema: { + input: commonGitlabConfig.merge( + z.object({ + path: z + .array(z.string(), { + description: 'A path of group names that is ensured to exist', + }) + .min(1), + }), + ), + output: z.object({ + groupId: z + .number({ description: 'The id of the innermost sub-group' }) + .optional(), + }), + }, + async handler(ctx) { + const { path } = ctx.input; + const { token, integrationConfig } = getToken(ctx.input, integrations); + + const api = new Gitlab({ + host: integrationConfig.config.baseUrl, + token: token, + }); + + let currentPath: string = 'repos'; + let parent: GroupSchema | null = null; + for (const pathElement of path) { + const fullPath = `${currentPath}/${pathElement}`; + const result = (await api.Groups.search( + fullPath, + )) as any as Array; // recast since the return type for search is wrong in the gitbeaker typings + const subGroup = result.find( + searchPathElem => searchPathElem.full_path === fullPath, + ); + if (!subGroup) { + ctx.logger.info(`creating missing group ${fullPath}`); + parent = await api.Groups.create( + pathElement, + pathElement, + parent + ? { + parent_id: parent.id, + } + : {}, + ); + } else { + parent = subGroup; + } + currentPath = fullPath; + } + if (parent !== null) { + ctx.output('groupId', parent?.id); + } + }, + }); +}; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts index 0f39e3bc33..614de0e221 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts @@ -16,10 +16,12 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import commonGitlabConfig from '../commonGitlabConfig'; import { getToken } from '../util'; +import { z } from 'zod'; /** - * Creates a `gitlab:create-project-access-token` Scaffolder action. + * Creates a `gitlab:projectAccessToken:create` Scaffolder action. * * @param options - Templating configuration. * @public @@ -28,65 +30,29 @@ export const createGitlabProjectAccessTokenAction = (options: { integrations: ScmIntegrationRegistry; }) => { const { integrations } = options; - return createTemplateAction<{ - repoUrl: string; - projectId: string | number; - name: string; - accessLevel: number; - scopes: string[]; - token?: string; - }>({ + return createTemplateAction({ id: 'gitlab:projectAccessToken:create', schema: { - input: { - required: ['projectId', 'repoUrl'], - type: 'object', - properties: { - repoUrl: { - title: 'Repository Location', - type: 'string', - }, - projectId: { - title: 'Project ID', - type: ['string', 'number'], - }, - name: { - title: 'Deploy Token Name', - type: 'string', - }, - accessLevel: { - title: 'Access Level of the Token', - type: 'number', - }, - scopes: { - title: 'Scopes', - type: 'array', - }, - token: { - title: 'Authentication Token', - type: 'string', - description: 'The token to use for authorization to GitLab', - }, - }, - }, - output: { - type: 'object', - properties: { - access_token: { - title: 'Access Token', - type: 'string', - }, - }, - }, + input: commonGitlabConfig.merge( + z.object({ + projectId: z.union([z.number(), z.string()], { + description: 'Project ID', + }), + name: z.string({ description: 'Deploy Token Name' }).optional(), + accessLevel: z + .number({ description: 'Access Level of the Token' }) + .optional(), + scopes: z.array(z.string(), { description: 'Scopes' }).optional(), + }), + ), + output: z.object({ + access_token: z.string({ description: 'Access Token' }), + }), }, async handler(ctx) { ctx.logger.info(`Creating Token for Project "${ctx.input.projectId}"`); - const { repoUrl, projectId, name, accessLevel, scopes } = ctx.input; - const { token, integrationConfig } = getToken( - repoUrl, - ctx.input.token, - integrations, - ); + const { projectId, name, accessLevel, scopes } = ctx.input; + const { token, integrationConfig } = getToken(ctx.input, integrations); const response = await fetch( `${integrationConfig.config.baseUrl}/api/v4/projects/${projectId}/access_tokens`, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts index 35a53c87e2..1665d23b39 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts @@ -18,11 +18,13 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Gitlab } from '@gitbeaker/node'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { DeployTokenScope } from '@gitbeaker/core/dist/types/templates/ResourceDeployTokens'; +import commonGitlabConfig from '../commonGitlabConfig'; import { getToken } from '../util'; import { InputError } from '@backstage/errors'; +import { z } from 'zod'; /** - * Creates a `gitlab:create-project-deploy-token` Scaffolder action. + * Creates a `gitlab:projectDeployToken:create` Scaffolder action. * * @param options - Templating configuration. * @public @@ -31,69 +33,30 @@ export const createGitlabProjectDeployTokenAction = (options: { integrations: ScmIntegrationRegistry; }) => { const { integrations } = options; - return createTemplateAction<{ - repoUrl: string; - projectId: string | number; - name: string; - username: string; - scopes: string[]; - token?: string; - }>({ + return createTemplateAction({ id: 'gitlab:projectDeployToken:create', schema: { - input: { - required: ['projectId', 'repoUrl'], - type: 'object', - properties: { - repoUrl: { - title: 'Repository Location', - type: 'string', - }, - projectId: { - title: 'Project ID', - type: ['string', 'number'], - }, - name: { - title: 'Deploy Token Name', - type: 'string', - }, - username: { - title: 'Deploy Token Username', - type: 'string', - }, - scopes: { - title: 'Scopes', - type: 'array', - }, - token: { - title: 'Authentication Token', - type: 'string', - description: 'The token to use for authorization to GitLab', - }, - }, - }, - output: { - type: 'object', - properties: { - deploy_token: { - title: 'Deploy Token', - type: 'string', - }, - user: { - title: 'User', - type: 'string', - }, - }, - }, + input: commonGitlabConfig.merge( + z.object({ + projectId: z.union([z.number(), z.string()], { + description: 'Project ID', + }), + name: z.string({ description: 'Deploy Token Name' }), + username: z + .string({ description: 'Deploy Token Username' }) + .optional(), + scopes: z.array(z.string(), { description: 'Scopes' }).optional(), + }), + ), + output: z.object({ + deploy_token: z.string({ description: 'Deploy Token' }), + user: z.string({ description: 'User' }), + }), }, async handler(ctx) { ctx.logger.info(`Creating Token for Project "${ctx.input.projectId}"`); - const { repoUrl, projectId, name, username, scopes } = ctx.input; - const { token, integrationConfig } = getToken( - repoUrl, - ctx.input.token, - integrations, - ); + const { projectId, name, username, scopes } = ctx.input; + const { token, integrationConfig } = getToken(ctx.input, integrations); const api = new Gitlab({ host: integrationConfig.config.baseUrl, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts index 847bbc2057..a154c16662 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts @@ -16,11 +16,13 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { getToken } from '../util'; import { Gitlab } from '@gitbeaker/node'; +import { getToken } from '../util'; +import commonGitlabConfig from '../commonGitlabConfig'; +import { z } from 'zod'; /** - * Creates a `gitlab:create-project-variable` Scaffolder action. + * Creates a `gitlab:projectVariable:create` Scaffolder action. * * @param options - Templating configuration. * @public @@ -29,96 +31,55 @@ export const createGitlabProjectVariableAction = (options: { integrations: ScmIntegrationRegistry; }) => { const { integrations } = options; - return createTemplateAction<{ - repoUrl: string; - projectId: string | number; - key: string; - value: string; - variableType: string; - variableProtected: boolean; - masked: boolean; - raw: boolean; - environmentScope: string; - token?: string; - }>({ + return createTemplateAction({ id: 'gitlab:projectVariable:create', schema: { - input: { - required: [ - 'repoUrl', - 'projectId', - 'key', - 'value', - 'variableType', - 'variableProtected', - 'masked', - 'raw', - 'environmentScope', - ], - type: 'object', - properties: { - repoUrl: { - title: 'Repository Location', - type: 'string', - }, - projectId: { - title: 'Project ID', - type: ['string', 'number'], - }, - key: { - title: - 'The key of a variable; must have no more than 255 characters; only A-Z, a-z, 0-9, and _ are allowed', - type: 'string', - }, - value: { - title: 'The value of a variable', - type: 'string', - }, - variableType: { - title: 'Variable Type (env_var or file)', - type: 'string', - }, - variableProtected: { - title: 'Whether the variable is protected. Default: false', - type: 'boolean', - }, - masked: { - title: 'Whether the variable is masked. Default: false', - type: 'boolean', - }, - raw: { - title: 'Whether the variable is expandable. Default: false', - type: 'boolean', - }, - environmentScope: { - title: 'The environment_scope of the variable. Default: *', - type: 'string', - }, - token: { - title: 'Authentication Token', - type: 'string', - description: 'The token to use for authorization to GitLab', - }, - }, - }, + input: commonGitlabConfig.merge( + z.object({ + projectId: z.union([z.number(), z.string()], { + description: 'Project ID', + }), + key: z + .string({ + description: + 'The key of a variable; must have no more than 255 characters; only A-Z, a-z, 0-9, and _ are allowed', + }) + .regex(/^[A-Za-z0-9_]{1,255}$/), + value: z.string({ description: 'The value of a variable' }), + variableType: z.string({ + description: 'Variable Type (env_var or file)', + }), + variableProtected: z + .boolean({ description: 'Whether the variable is protected' }) + .default(false) + .optional(), + masked: z + .boolean({ description: 'Whether the variable is masked' }) + .default(false) + .optional(), + raw: z + .boolean({ description: 'Whether the variable is expandable' }) + .default(false) + .optional(), + environmentScope: z + .string({ description: 'The environment_scope of the variable' }) + .default('*') + .optional(), + }), + ), }, async handler(ctx) { const { - repoUrl, projectId, key, value, variableType, - variableProtected, - masked, - raw, - environmentScope, + variableProtected = false, + masked = false, + raw = false, + environmentScope = '*', } = ctx.input; - const { token, integrationConfig } = getToken( - repoUrl, - ctx.input.token, - integrations, - ); + const { token, integrationConfig } = getToken(ctx.input, integrations); const api = new Gitlab({ host: integrationConfig.config.baseUrl, diff --git a/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts b/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts new file mode 100644 index 0000000000..02724a3674 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; + +const commonGitlabConfig = z.object({ + repoUrl: z.string({ description: 'Repository Location' }), + token: z + .string({ description: 'The token to use for authorization to GitLab' }) + .optional(), +}); + +export default commonGitlabConfig; diff --git a/plugins/scaffolder-backend-module-gitlab/src/index.ts b/plugins/scaffolder-backend-module-gitlab/src/index.ts index 11209e19bd..4fd4dee35b 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/index.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/index.ts @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /** - * A module for the scaffolder backend that lets you create gitlab project access tokens or deploy tokens + * A module for the scaffolder backend that lets you interact with gitlab * * @packageDocumentation */ - +export * from './actions/createGitlabGroupEnsureExistsAction'; export * from './actions/createGitlabProjectDeployTokenAction'; export * from './actions/createGitlabProjectAccessTokenAction'; export * from './actions/createGitlabProjectVariableAction'; diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts index fc6667d6d3..13c95e48e8 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts @@ -19,6 +19,8 @@ import { GitLabIntegration, ScmIntegrationRegistry, } from '@backstage/integration'; +import { z } from 'zod'; +import commonGitlabConfig from './commonGitlabConfig'; export const parseRepoHost = (repoUrl: string): string => { let parsed; @@ -33,11 +35,10 @@ export const parseRepoHost = (repoUrl: string): string => { }; export const getToken = ( - repoUrl: string, - inputToken: string | null | undefined, + config: z.infer, integrations: ScmIntegrationRegistry, ): { token: string; integrationConfig: GitLabIntegration } => { - const host = parseRepoHost(repoUrl); + const host = parseRepoHost(config.repoUrl); const integrationConfig = integrations.gitlab.byHost(host); if (!integrationConfig) { @@ -46,8 +47,8 @@ export const getToken = ( ); } - const token = inputToken || integrationConfig.config.token!; - const tokenType = inputToken ? 'oauthToken' : 'token'; + const token = config.token || integrationConfig.config.token!; + const tokenType = config.token ? 'oauthToken' : 'token'; if (tokenType === 'oauthToken') { throw new InputError(`OAuth Token is currently not supported`); diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index cd815f8d1e..bd263d65a7 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.13.2-next.2 + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + ## 0.4.14-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 035d698089..e45fc153ae 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.4.14-next.1", + "version": "0.4.14-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index 128526ac64..4206d39413 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + ## 0.1.5-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index fa851fd80e..d66fea6d60 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-sentry", - "version": "0.1.5-next.1", + "version": "0.1.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index b70c32aa52..232510d721 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + ## 0.2.18-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 77f50e3130..952a220c5a 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.2.18-next.1", + "version": "0.2.18-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 0b5b88b148..221362e637 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend +## 1.13.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.9.1-next.2 + - @backstage/plugin-scaffolder-common@1.3.0-next.0 + - @backstage/plugin-scaffolder-node@0.1.3-next.2 + - @backstage/config@1.0.7 + ## 1.13.2-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index a6fd41a29e..07dd5eaf1d 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -29,6 +29,7 @@ import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { RESOURCE_TYPE_SCAFFOLDER_ACTION } from '@backstage/plugin-scaffolder-common/alpha'; import { RESOURCE_TYPE_SCAFFOLDER_TEMPLATE } from '@backstage/plugin-scaffolder-common/alpha'; import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -49,6 +50,16 @@ import { ZodTypeDef } from 'zod'; // @public @deprecated (undocumented) export type ActionContext = ActionContext_2; +// @public (undocumented) +export type ActionPermissionRuleInput< + TParams extends PermissionRuleParams = PermissionRuleParams, +> = PermissionRule< + TemplateEntityStepV1beta3 | TemplateParametersV1beta3, + {}, + typeof RESOURCE_TYPE_SCAFFOLDER_ACTION, + TParams +>; + // @public export const createBuiltinActions: ( options: CreateBuiltInActionsOptions, @@ -108,6 +119,8 @@ export function createFetchCatalogEntityAction(options: { entityRef?: string | undefined; entityRefs?: string[] | undefined; optional?: boolean | undefined; + defaultKind?: string | undefined; + defaultNamespace?: string | undefined; }, { entity?: any; @@ -763,7 +776,9 @@ export interface RouterOptions { // (undocumented) logger: Logger; // (undocumented) - permissionRules?: TemplatePermissionRuleInput[]; + permissionRules?: Array< + TemplatePermissionRuleInput | ActionPermissionRuleInput + >; // (undocumented) permissions?: PermissionEvaluator; // (undocumented) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 5b9e28322d..3d1794410d 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "1.13.2-next.1", + "version": "1.13.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -92,7 +92,7 @@ "p-queue": "^6.6.2", "prom-client": "^14.0.1", "uuid": "^8.2.0", - "vm2": "^3.9.17", + "vm2": "^3.9.18", "winston": "^3.2.1", "yaml": "^2.0.0", "zen-observable": "^0.10.0", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts index 1cce7d66cb..bed15f2e39 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts @@ -46,179 +46,249 @@ describe('catalog:fetch', () => { jest.resetAllMocks(); }); - it('should return entity from catalog', async () => { - getEntityByRef.mockReturnValueOnce({ - metadata: { - namespace: 'default', - name: 'test', - }, - kind: 'Component', - } as Entity); - - await action.handler({ - ...mockContext, - input: { - entityRef: 'component:default/test', - }, - }); - - expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { - token: 'secret', - }); - expect(mockContext.output).toHaveBeenCalledWith('entity', { - metadata: { - namespace: 'default', - name: 'test', - }, - kind: 'Component', - }); - }); - - it('should throw error if entity fetch fails from catalog and optional is false', async () => { - getEntityByRef.mockImplementationOnce(() => { - throw new Error('Not found'); - }); - - await expect( - action.handler({ - ...mockContext, - input: { - entityRef: 'component:default/test', - }, - }), - ).rejects.toThrow('Not found'); - - expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { - token: 'secret', - }); - expect(mockContext.output).not.toHaveBeenCalled(); - }); - - it('should throw error if entity not in catalog and optional is false', async () => { - getEntityByRef.mockReturnValueOnce(null); - - await expect( - action.handler({ - ...mockContext, - input: { - entityRef: 'component:default/test', - }, - }), - ).rejects.toThrow('Entity component:default/test not found'); - - expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { - token: 'secret', - }); - expect(mockContext.output).not.toHaveBeenCalled(); - }); - - it('should return entities from catalog', async () => { - getEntitiesByRefs.mockReturnValueOnce({ - items: [ - { - metadata: { - namespace: 'default', - name: 'test', - }, - kind: 'Component', - } as Entity, - ], - }); - - await action.handler({ - ...mockContext, - input: { - entityRefs: ['component:default/test'], - }, - }); - - expect(getEntitiesByRefs).toHaveBeenCalledWith( - { entityRefs: ['component:default/test'] }, - { - token: 'secret', - }, - ); - expect(mockContext.output).toHaveBeenCalledWith('entities', [ - { + describe('fetch single entity', () => { + it('should return entity from catalog', async () => { + getEntityByRef.mockReturnValueOnce({ metadata: { namespace: 'default', name: 'test', }, kind: 'Component', - }, - ]); + } as Entity); + + await action.handler({ + ...mockContext, + input: { + entityRef: 'component:default/test', + }, + }); + + expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { + token: 'secret', + }); + expect(mockContext.output).toHaveBeenCalledWith('entity', { + metadata: { + namespace: 'default', + name: 'test', + }, + kind: 'Component', + }); + }); + + it('should throw error if entity fetch fails from catalog and optional is false', async () => { + getEntityByRef.mockImplementationOnce(() => { + throw new Error('Not found'); + }); + + await expect( + action.handler({ + ...mockContext, + input: { + entityRef: 'component:default/test', + }, + }), + ).rejects.toThrow('Not found'); + + expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { + token: 'secret', + }); + expect(mockContext.output).not.toHaveBeenCalled(); + }); + + it('should throw error if entity not in catalog and optional is false', async () => { + getEntityByRef.mockReturnValueOnce(null); + + await expect( + action.handler({ + ...mockContext, + input: { + entityRef: 'component:default/test', + }, + }), + ).rejects.toThrow('Entity component:default/test not found'); + + expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { + token: 'secret', + }); + expect(mockContext.output).not.toHaveBeenCalled(); + }); + + it('should use defaultKind and defaultNamespace if provided', async () => { + const entity: Entity = { + metadata: { + namespace: 'ns', + name: 'test', + }, + kind: 'Group', + } as Entity; + getEntityByRef.mockReturnValueOnce(entity); + + await action.handler({ + ...mockContext, + input: { + entityRef: 'test', + defaultKind: 'Group', + defaultNamespace: 'ns', + }, + }); + + expect(getEntityByRef).toHaveBeenCalledWith('group:ns/test', { + token: 'secret', + }); + expect(mockContext.output).toHaveBeenCalledWith('entity', entity); + }); }); - it('should throw error if undefined is returned for some entity', async () => { - getEntitiesByRefs.mockReturnValueOnce({ - items: [ + describe('fetch multiple entities', () => { + it('should return entities from catalog', async () => { + getEntitiesByRefs.mockReturnValueOnce({ + items: [ + { + metadata: { + namespace: 'default', + name: 'test', + }, + kind: 'Component', + } as Entity, + ], + }); + + await action.handler({ + ...mockContext, + input: { + entityRefs: ['component:default/test'], + }, + }); + + expect(getEntitiesByRefs).toHaveBeenCalledWith( + { entityRefs: ['component:default/test'] }, + { + token: 'secret', + }, + ); + expect(mockContext.output).toHaveBeenCalledWith('entities', [ { metadata: { namespace: 'default', name: 'test', }, kind: 'Component', - } as Entity, - undefined, - ], + }, + ]); }); - await expect( - action.handler({ + it('should throw error if undefined is returned for some entity', async () => { + getEntitiesByRefs.mockReturnValueOnce({ + items: [ + { + metadata: { + namespace: 'default', + name: 'test', + }, + kind: 'Component', + } as Entity, + undefined, + ], + }); + + await expect( + action.handler({ + ...mockContext, + input: { + entityRefs: ['component:default/test', 'component:default/test2'], + optional: false, + }, + }), + ).rejects.toThrow('Entity component:default/test2 not found'); + + expect(getEntitiesByRefs).toHaveBeenCalledWith( + { entityRefs: ['component:default/test', 'component:default/test2'] }, + { + token: 'secret', + }, + ); + expect(mockContext.output).not.toHaveBeenCalled(); + }); + + it('should return null in case some of the entities not found and optional is true', async () => { + getEntitiesByRefs.mockReturnValueOnce({ + items: [ + { + metadata: { + namespace: 'default', + name: 'test', + }, + kind: 'Component', + } as Entity, + undefined, + ], + }); + + await action.handler({ ...mockContext, input: { entityRefs: ['component:default/test', 'component:default/test2'], - optional: false, + optional: true, }, - }), - ).rejects.toThrow('Entity component:default/test2 not found'); + }); - expect(getEntitiesByRefs).toHaveBeenCalledWith( - { entityRefs: ['component:default/test', 'component:default/test2'] }, - { - token: 'secret', - }, - ); - expect(mockContext.output).not.toHaveBeenCalled(); - }); - - it('should return null in case some of the entities not found and optional is true', async () => { - getEntitiesByRefs.mockReturnValueOnce({ - items: [ + expect(getEntitiesByRefs).toHaveBeenCalledWith( + { entityRefs: ['component:default/test', 'component:default/test2'] }, + { + token: 'secret', + }, + ); + expect(mockContext.output).toHaveBeenCalledWith('entities', [ { metadata: { namespace: 'default', name: 'test', }, kind: 'Component', - } as Entity, - undefined, - ], + }, + null, + ]); }); - await action.handler({ - ...mockContext, - input: { - entityRefs: ['component:default/test', 'component:default/test2'], - optional: true, - }, - }); - - expect(getEntitiesByRefs).toHaveBeenCalledWith( - { entityRefs: ['component:default/test', 'component:default/test2'] }, - { - token: 'secret', - }, - ); - expect(mockContext.output).toHaveBeenCalledWith('entities', [ - { + it('should use defaultKind and defaultNamespace if provided', async () => { + const entity1: Entity = { + metadata: { + namespace: 'ns', + name: 'test', + }, + kind: 'Group', + } as Entity; + const entity2: Entity = { metadata: { namespace: 'default', name: 'test', }, - kind: 'Component', - }, - null, - ]); + kind: 'User', + } as Entity; + getEntitiesByRefs.mockReturnValueOnce({ + items: [entity1, entity2], + }); + + await action.handler({ + ...mockContext, + input: { + entityRefs: ['test', 'user:default/test'], + defaultKind: 'Group', + defaultNamespace: 'ns', + }, + }); + + expect(getEntitiesByRefs).toHaveBeenCalledWith( + { entityRefs: ['group:ns/test', 'user:default/test'] }, + { + token: 'secret', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('entities', [ + entity1, + entity2, + ]); + }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts index d6abeee792..437da8ae73 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts @@ -18,6 +18,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; import { z } from 'zod'; +import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; const id = 'catalog:fetch'; @@ -69,6 +70,7 @@ export function createFetchCatalogEntityAction(options: { description: 'Returns entity or entities from the catalog by entity reference(s)', examples, + supportsDryRun: true, schema: { input: z.object({ entityRef: z @@ -87,6 +89,10 @@ export function createFetchCatalogEntityAction(options: { 'Allow the entity or entities to optionally exist. Default: false', }) .optional(), + defaultKind: z.string({ description: 'The default kind' }).optional(), + defaultNamespace: z + .string({ description: 'The default namespace' }) + .optional(), }), output: z.object({ entity: z @@ -106,7 +112,8 @@ export function createFetchCatalogEntityAction(options: { }), }, async handler(ctx) { - const { entityRef, entityRefs, optional } = ctx.input; + const { entityRef, entityRefs, optional, defaultKind, defaultNamespace } = + ctx.input; if (!entityRef && !entityRefs) { if (optional) { return; @@ -115,9 +122,14 @@ export function createFetchCatalogEntityAction(options: { } if (entityRef) { - const entity = await catalogClient.getEntityByRef(entityRef, { - token: ctx.secrets?.backstageToken, - }); + const entity = await catalogClient.getEntityByRef( + stringifyEntityRef( + parseEntityRef(entityRef, { defaultKind, defaultNamespace }), + ), + { + token: ctx.secrets?.backstageToken, + }, + ); if (!entity && !optional) { throw new Error(`Entity ${entityRef} not found`); @@ -127,7 +139,13 @@ export function createFetchCatalogEntityAction(options: { if (entityRefs) { const entities = await catalogClient.getEntitiesByRefs( - { entityRefs }, + { + entityRefs: entityRefs.map(ref => + stringifyEntityRef( + parseEntityRef(ref, { defaultKind, defaultNamespace }), + ), + ), + }, { token: ctx.secrets?.backstageToken, }, diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index deff4392d6..1771067367 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -36,8 +36,10 @@ import { TemplateEntityStepV1beta3, } from '@backstage/plugin-scaffolder-common'; import { + RESOURCE_TYPE_SCAFFOLDER_ACTION, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, - scaffolderPermissions, + scaffolderActionPermissions, + scaffolderTemplatePermissions, templateParameterReadPermission, templateStepReadPermission, } from '@backstage/plugin-scaffolder-common/alpha'; @@ -71,7 +73,7 @@ import { createPermissionIntegrationRouter, PermissionRule, } from '@backstage/plugin-permission-node'; -import { scaffolderTemplateRules } from './rules'; +import { scaffolderActionRules, scaffolderTemplateRules } from './rules'; /** * @@ -85,6 +87,29 @@ export type TemplatePermissionRuleInput< typeof RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, TParams >; +function isTemplatePermissionRuleInput( + permissionRule: TemplatePermissionRuleInput | ActionPermissionRuleInput, +): permissionRule is TemplatePermissionRuleInput { + return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_TEMPLATE; +} + +/** + * + * @public + */ +export type ActionPermissionRuleInput< + TParams extends PermissionRuleParams = PermissionRuleParams, +> = PermissionRule< + TemplateEntityStepV1beta3 | TemplateParametersV1beta3, + {}, + typeof RESOURCE_TYPE_SCAFFOLDER_ACTION, + TParams +>; +function isActionPermissionRuleInput( + permissionRule: TemplatePermissionRuleInput | ActionPermissionRuleInput, +): permissionRule is ActionPermissionRuleInput { + return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_ACTION; +} /** * RouterOptions @@ -113,7 +138,9 @@ export interface RouterOptions { additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; permissions?: PermissionEvaluator; - permissionRules?: TemplatePermissionRuleInput[]; + permissionRules?: Array< + TemplatePermissionRuleInput | ActionPermissionRuleInput + >; identity?: IdentityApi; } @@ -290,17 +317,32 @@ export async function createRouter( const templateRules: TemplatePermissionRuleInput[] = Object.values( scaffolderTemplateRules, ); + const actionRules: ActionPermissionRuleInput[] = Object.values( + scaffolderActionRules, + ); if (permissionRules) { - templateRules.push(...permissionRules); + templateRules.push( + ...permissionRules.filter(isTemplatePermissionRuleInput), + ); + actionRules.push(...permissionRules.filter(isActionPermissionRuleInput)); } const isAuthorized = createConditionAuthorizer(Object.values(templateRules)); const permissionIntegrationRouter = createPermissionIntegrationRouter({ - resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, - permissions: scaffolderPermissions, - rules: templateRules, + resources: [ + { + resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, + permissions: scaffolderTemplatePermissions, + rules: templateRules, + }, + { + resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION, + permissions: scaffolderActionPermissions, + rules: actionRules, + }, + ], }); router.use(permissionIntegrationRouter); diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index c317230580..b09bf55eb3 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-scaffolder-common +## 1.3.0-next.0 + +### Minor Changes + +- 82e10a6939c: Add support for Markdown text blob outputs from templates + ## 1.2.7 ### Patch Changes diff --git a/plugins/scaffolder-common/alpha-api-report.md b/plugins/scaffolder-common/alpha-api-report.md index d6e746026a..de9dffd1b9 100644 --- a/plugins/scaffolder-common/alpha-api-report.md +++ b/plugins/scaffolder-common/alpha-api-report.md @@ -14,12 +14,18 @@ export const RESOURCE_TYPE_SCAFFOLDER_ACTION = 'scaffolder-action'; // @alpha export const RESOURCE_TYPE_SCAFFOLDER_TEMPLATE = 'scaffolder-template'; +// @alpha +export const scaffolderActionPermissions: ResourcePermission<'scaffolder-action'>[]; + // @alpha export const scaffolderPermissions: ( | ResourcePermission<'scaffolder-action'> | ResourcePermission<'scaffolder-template'> )[]; +// @alpha +export const scaffolderTemplatePermissions: ResourcePermission<'scaffolder-template'>[]; + // @alpha export const templateParameterReadPermission: ResourcePermission<'scaffolder-template'>; diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 6dfe10121e..c12f16dbad 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "1.2.7", + "version": "1.3.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index beb0ca5e31..1de68205b2 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -218,6 +218,33 @@ } } } + }, + "text": { + "type": "array", + "description": "A list of Markdown text blobs, like output data from the template.", + "items": { + "type": "object", + "required": [], + "properties": { + "title": { + "type": "string", + "description": "A user friendly display name for the text.", + "examples": ["Output Content"], + "minLength": 1 + }, + "icon": { + "type": "string", + "description": "A key representing a visual icon to be displayed in the UI.", + "examples": ["dashboard"], + "minLength": 1 + }, + "content": { + "type": "string", + "description": "The text blob to display in the UI, rendered as Markdown.", + "examples": ["**hey** _I'm_ Markdown"] + } + } + } } }, "additionalProperties": { diff --git a/plugins/scaffolder-common/src/permissions.ts b/plugins/scaffolder-common/src/permissions.ts index f06d7984f2..5c4c367889 100644 --- a/plugins/scaffolder-common/src/permissions.ts +++ b/plugins/scaffolder-common/src/permissions.ts @@ -87,3 +87,18 @@ export const scaffolderPermissions = [ templateParameterReadPermission, templateStepReadPermission, ]; + +/** + * List of the scaffolder permissions that are associated with template steps and parameters. + * @alpha + */ +export const scaffolderTemplatePermissions = [ + templateParameterReadPermission, + templateStepReadPermission, +]; + +/** + * List of the scaffolder permissions that are associated with scaffolder actions. + * @alpha + */ +export const scaffolderActionPermissions = [actionExecutePermission]; diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index 796c157f7a..5936463ac8 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-scaffolder-node +## 0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-common@1.3.0-next.0 + ## 0.1.3-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index f1e49a52e1..d00dd15226 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-node", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", - "version": "0.1.3-next.1", + "version": "0.1.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 402c552c79..8d93049843 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-scaffolder-react +## 1.4.0-next.2 + +### Minor Changes + +- 82e10a6939c: Add support for Markdown text blob outputs from templates + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/plugin-scaffolder-common@1.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 1.3.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index baf5afb312..1761c88edf 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -217,6 +217,13 @@ export type ScaffolderOutputLink = { entityRef?: string; }; +// @public (undocumented) +export type ScaffolderOutputText = { + title?: string; + icon?: string; + content?: string; +}; + // @public export interface ScaffolderScaffoldOptions { // (undocumented) @@ -261,6 +268,7 @@ export type ScaffolderTask = { // @public (undocumented) export type ScaffolderTaskOutput = { links?: ScaffolderOutputLink[]; + text?: ScaffolderOutputText[]; } & { [key: string]: unknown; }; diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 571b356a2d..c455eeebf6 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-react", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", - "version": "1.3.1-next.1", + "version": "1.4.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index b39555c14f..2adecdd0d2 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -84,9 +84,17 @@ export type ScaffolderOutputLink = { entityRef?: string; }; +/** @public */ +export type ScaffolderOutputText = { + title?: string; + icon?: string; + content?: string; +}; + /** @public */ export type ScaffolderTaskOutput = { links?: ScaffolderOutputLink[]; + text?: ScaffolderOutputText[]; } & { [key: string]: unknown; }; diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx new file mode 100644 index 0000000000..8366dcadf8 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { DefaultTemplateOutputs } from '.'; + +describe('', () => { + it('should render template output', async () => { + const output = { + links: [{ title: 'Link 1', url: 'https://backstage.io/' }], + text: [{ title: 'Text 1', content: 'Hello, **world**!' }], + }; + + const { getByRole } = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + // test link outputs + for (const link of output.links ?? []) { + expect( + getByRole('button', { name: link.title }).closest('a'), + ).toHaveAttribute('href', link.url); + } + + // test text outputs + for (const text of output.text ?? []) { + await act(async () => { + fireEvent.click(getByRole('button', { name: text.title })); + }); + + expect(getByRole('heading', { level: 2 }).innerHTML).toBe(text.title); + } + }); +}); diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index 4d908461b0..a495e4ca9e 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { Box, Paper } from '@material-ui/core'; -import { LinkOutputs } from './LinkOutputs'; +import { InfoCard, MarkdownContent } from '@backstage/core-components'; import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; +import { Box, Paper } from '@material-ui/core'; +import React, { useMemo, useState } from 'react'; +import { LinkOutputs } from './LinkOutputs'; +import { TextOutputs } from './TextOutputs'; /** * The DefaultOutputs renderer for the scaffolder task output @@ -26,17 +28,47 @@ import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; export const DefaultTemplateOutputs = (props: { output?: ScaffolderTaskOutput; }) => { - if (!props.output?.links) { + const [textOutputIndex, setTextOutputIndex] = useState(); + + const textOutput = useMemo( + () => + textOutputIndex !== undefined + ? props.output?.text?.[textOutputIndex] + : null, + [props.output, textOutputIndex], + ); + + if (!props.output) { return null; } return ( - - - - + <> + + + + + + + + + {textOutput ? ( + + + + + + - - + ) : null} + ); }; diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx new file mode 100644 index 0000000000..7e64603e92 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { IconComponent, useApp } from '@backstage/core-plugin-api'; +import { Button } from '@material-ui/core'; +import WebIcon from '@material-ui/icons/Web'; +import React from 'react'; +import { ScaffolderTaskOutput } from '../../../api'; + +export const TextOutputs = (props: { + output: ScaffolderTaskOutput; + index?: number; + setIndex?: (index: number | undefined) => void; +}) => { + const { + output: { text = [] }, + index, + setIndex, + } = props; + + const app = useApp(); + + const iconResolver = (key?: string): IconComponent => + app.getSystemIcon(key!) ?? WebIcon; + + return ( + <> + {text + .filter(({ content }) => content !== undefined) + .map(({ title, icon }, i) => { + const Icon = iconResolver(icon); + return ( + + ); + })} + + ); +}; diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 11cc2224aa..577517a127 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-scaffolder +## 1.13.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/plugin-scaffolder-common@1.3.0-next.0 + - @backstage/plugin-scaffolder-react@1.4.0-next.2 + - @backstage/core-components@0.13.1-next.1 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-permission-react@0.4.12 + ## 1.13.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 2d618d6960..4b5e34e5de 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "1.13.1-next.1", + "version": "1.13.1-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index f5c5d9acd5..a74c8e5c0d 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.3.0-next.2 + +### Minor Changes + +- 3d72bdb41c7: Upgrade to aws-sdk v3 and include OpenSearch Serverless support + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.7 + - @backstage/integration-aws-node@0.1.2 + ## 1.2.1-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index c7add1e8cb..b567414b22 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -8,8 +8,8 @@ import { ApiResponse } from '@opensearch-project/opensearch'; import { ApiResponse as ApiResponse_2 } from '@elastic/elasticsearch'; import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; -import { BulkHelper } from '@opensearch-project/opensearch/lib/Helpers'; -import { BulkStats } from '@opensearch-project/opensearch/lib/Helpers'; +import { BulkHelper } from '@elastic/elasticsearch/lib/Helpers'; +import { BulkStats } from '@elastic/elasticsearch/lib/Helpers'; import { Config } from '@backstage/config'; import type { ConnectionOptions } from 'tls'; import { IndexableDocument } from '@backstage/plugin-search-common'; @@ -424,6 +424,10 @@ export interface OpenSearchElasticSearchClientOptions nodes?: string | string[] | OpenSearchNodeOptions | OpenSearchNodeOptions[]; // (undocumented) provider?: 'aws' | 'opensearch'; + // (undocumented) + region?: string; + // (undocumented) + service?: 'es' | 'aoss'; } // @public (undocumented) diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 73c3a5d7dc..20f12c3592 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "1.2.1-next.1", + "version": "1.3.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,12 +39,12 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/integration-aws-node": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@elastic/elasticsearch": "^7.13.0", - "@opensearch-project/opensearch": "^2.0.0", - "aws-os-connection": "^0.2.0", - "aws-sdk": "^2.948.0", + "@opensearch-project/opensearch": "^2.2.1", + "aws4": "^1.12.0", "elastic-builder": "^2.16.0", "lodash": "^4.17.21", "uuid": "^8.3.2", diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientOptions.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientOptions.ts index 8cb2f67fce..2974f03331 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientOptions.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientOptions.ts @@ -51,6 +51,8 @@ export type ElasticSearchClientOptions = export interface OpenSearchElasticSearchClientOptions extends BaseElasticSearchClientOptions { provider?: 'aws' | 'opensearch'; + region?: string; + service?: 'es' | 'aoss'; auth?: OpenSearchAuth; connection?: OpenSearchConnectionConstructor; node?: string | string[] | OpenSearchNodeOptions | OpenSearchNodeOptions[]; diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.test.ts index 7355daf3b9..75f2e36b2b 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.test.ts @@ -14,17 +14,13 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; import { Client as ElasticSearchClient } from '@elastic/elasticsearch'; import { Client as OpenSearchClient } from '@opensearch-project/opensearch'; import Mock from '@short.io/opensearch-mock'; import { Readable } from 'stream'; import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper'; -import { - createElasticSearchClientOptions, - ElasticSearchClientOptions, -} from './ElasticSearchSearchEngine'; +import { ElasticSearchClientOptions } from './ElasticSearchSearchEngine'; jest.mock('@elastic/elasticsearch', () => ({ ...jest.requireActual('@elastic/elasticsearch'), @@ -103,11 +99,9 @@ describe('ElasticSearchClientWrapper', () => { let esOptions: ElasticSearchClientOptions; beforeEach(async () => { - esOptions = await createElasticSearchClientOptions( - new ConfigReader({ - node: 'http://localhost:9200', - }), - ); + esOptions = { + node: 'http://localhost:9200', + }; jest.clearAllMocks(); }); @@ -229,6 +223,7 @@ describe('ElasticSearchClientWrapper', () => { osOptions = { provider: 'aws', node: 'https://my-es-cluster.eu-west-1.es.amazonaws.com', + region: 'eu-west-1', // todo(backstage/techdocs-core): Remove the following ts-ignore when // @short.io/opensearch-mock is updated to work w/opensearch >= 2.0.0 // @ts-ignore diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 3294ca9d33..90f6f407d9 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -21,9 +21,10 @@ import { SearchEngine, SearchQuery, } from '@backstage/plugin-search-common'; -import { awsGetCredentials, createAWSConnection } from 'aws-os-connection'; import { isEmpty, isNumber, isNaN as nan } from 'lodash'; +import { AwsSigv4Signer } from '@opensearch-project/opensearch/aws'; +import { RequestSigner } from 'aws4'; import { Config } from '@backstage/config'; import { ElasticSearchClientOptions } from './ElasticSearchClientOptions'; import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper'; @@ -33,6 +34,10 @@ import { Logger } from 'winston'; import { MissingIndexError } from '@backstage/plugin-search-backend-node'; import esb from 'elastic-builder'; import { v4 as uuid } from 'uuid'; +import { + AwsCredentialProvider, + DefaultAwsCredentialsManager, +} from '@backstage/integration-aws-node'; export type { ElasticSearchClientOptions }; @@ -145,8 +150,9 @@ export class ElasticSearchSearchEngine implements SearchEngine { aliasPostfix = `search`, indexPrefix = ``, } = options; - - const clientOptions = await createElasticSearchClientOptions( + const credentialProvider = DefaultAwsCredentialsManager.fromConfig(config); + const clientOptions = await this.createElasticSearchClientOptions( + await credentialProvider?.getCredentialProvider(), config.getConfig('search.elasticsearch'), ); if (clientOptions.provider === 'elastic') { @@ -412,6 +418,106 @@ export class ElasticSearchSearchEngine implements SearchEngine { const postFix = this.aliasPostfix ? `__${this.aliasPostfix}` : ''; return `${this.indexPrefix}${type}${postFix}`; } + + private static async createElasticSearchClientOptions( + credentialProvider: AwsCredentialProvider, + config?: Config, + ): Promise { + if (!config) { + throw new Error('No elastic search config found'); + } + const clientOptionsConfig = config.getOptionalConfig('clientOptions'); + const sslConfig = clientOptionsConfig?.getOptionalConfig('ssl'); + + if (config.getOptionalString('provider') === 'elastic') { + const authConfig = config.getConfig('auth'); + return { + provider: 'elastic', + cloud: { + id: config.getString('cloudId'), + }, + auth: { + username: authConfig.getString('username'), + password: authConfig.getString('password'), + }, + ...(sslConfig + ? { + ssl: { + rejectUnauthorized: + sslConfig?.getOptionalBoolean('rejectUnauthorized'), + }, + } + : {}), + }; + } + if (config.getOptionalString('provider') === 'aws') { + const requestSigner = new RequestSigner(config.getString('node')); + const service = + config.getOptionalString('service') ?? requestSigner.service; + if (service !== 'es' && service !== 'aoss') + throw new Error(`Unrecognized serivce type: ${service}`); + return { + provider: 'aws', + node: config.getString('node'), + ...(sslConfig + ? { + ssl: { + rejectUnauthorized: + sslConfig?.getOptionalBoolean('rejectUnauthorized'), + }, + } + : {}), + ...AwsSigv4Signer({ + region: config.getOptionalString('region') ?? requestSigner.region, // for backwards compatibility + service: service, + getCredentials: async () => + await credentialProvider.sdkCredentialProvider(), + }), + }; + } + if (config.getOptionalString('provider') === 'opensearch') { + const authConfig = config.getConfig('auth'); + return { + provider: 'opensearch', + node: config.getString('node'), + auth: { + username: authConfig.getString('username'), + password: authConfig.getString('password'), + }, + ...(sslConfig + ? { + ssl: { + rejectUnauthorized: + sslConfig?.getOptionalBoolean('rejectUnauthorized'), + }, + } + : {}), + }; + } + const authConfig = config.getOptionalConfig('auth'); + const auth = + authConfig && + (authConfig.has('apiKey') + ? { + apiKey: authConfig.getString('apiKey'), + } + : { + username: authConfig.getString('username'), + password: authConfig.getString('password'), + }); + return { + node: config.getString('node'), + auth, + ...(sslConfig + ? { + ssl: { + rejectUnauthorized: + sslConfig?.getOptionalBoolean('rejectUnauthorized'), + }, + } + : {}), + }; + } } /** @@ -430,97 +536,3 @@ export function decodePageCursor(pageCursor?: string): { page: number } { export function encodePageCursor({ page }: { page: number }): string { return Buffer.from(`${page}`, 'utf-8').toString('base64'); } - -export async function createElasticSearchClientOptions( - config?: Config, -): Promise { - if (!config) { - throw new Error('No elastic search config found'); - } - const clientOptionsConfig = config.getOptionalConfig('clientOptions'); - const sslConfig = clientOptionsConfig?.getOptionalConfig('ssl'); - - if (config.getOptionalString('provider') === 'elastic') { - const authConfig = config.getConfig('auth'); - return { - provider: 'elastic', - cloud: { - id: config.getString('cloudId'), - }, - auth: { - username: authConfig.getString('username'), - password: authConfig.getString('password'), - }, - ...(sslConfig - ? { - ssl: { - rejectUnauthorized: - sslConfig?.getOptionalBoolean('rejectUnauthorized'), - }, - } - : {}), - }; - } - if (config.getOptionalString('provider') === 'aws') { - const awsCredentials = await awsGetCredentials(); - const AWSConnection = createAWSConnection(awsCredentials); - return { - provider: 'aws', - // todo(backstage/techdocs-core): Remove the following ts-ignore when - // aws-os-connection is updated to work with opensearch >= 2.0.0 - // @ts-ignore - node: config.getString('node'), - ...AWSConnection, - ...(sslConfig - ? { - ssl: { - rejectUnauthorized: - sslConfig?.getOptionalBoolean('rejectUnauthorized'), - }, - } - : {}), - }; - } - if (config.getOptionalString('provider') === 'opensearch') { - const authConfig = config.getConfig('auth'); - return { - provider: 'opensearch', - node: config.getString('node'), - auth: { - username: authConfig.getString('username'), - password: authConfig.getString('password'), - }, - ...(sslConfig - ? { - ssl: { - rejectUnauthorized: - sslConfig?.getOptionalBoolean('rejectUnauthorized'), - }, - } - : {}), - }; - } - const authConfig = config.getOptionalConfig('auth'); - const auth = - authConfig && - (authConfig.has('apiKey') - ? { - apiKey: authConfig.getString('apiKey'), - } - : { - username: authConfig.getString('username'), - password: authConfig.getString('password'), - }); - return { - node: config.getString('node'), - auth, - ...(sslConfig - ? { - ssl: { - rejectUnauthorized: - sslConfig?.getOptionalBoolean('rejectUnauthorized'), - }, - } - : {}), - }; -} diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 321fdefbc1..fbc53b4f2d 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend +## 1.3.1-next.2 + +### Patch Changes + +- 021cfbb5152: Added an OpenAPI 3.0 spec and enforced schema-first model on the router. +- Updated dependencies + - @backstage/config@1.0.7 + ## 1.3.1-next.1 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 5fa59077db..53059b2355 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "1.3.1-next.1", + "version": "1.3.1-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -57,6 +57,7 @@ "zod": "^3.21.4" }, "devDependencies": { + "@backstage/backend-openapi-utils": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", diff --git a/plugins/search-backend/src/schema/openapi.generated.ts b/plugins/search-backend/src/schema/openapi.generated.ts new file mode 100644 index 0000000000..b8f5f5b35e --- /dev/null +++ b/plugins/search-backend/src/schema/openapi.generated.ts @@ -0,0 +1,211 @@ +/* + * 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. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +export default { + openapi: '3.0.3', + info: { + title: '@backstage/plugin-search-backend', + version: '1', + description: + 'The Backstage backend plugin that provides search functionality.', + license: { + name: 'Apache-2.0', + url: 'http://www.apache.org/licenses/LICENSE-2.0.html', + }, + contact: {}, + }, + servers: [ + { + url: '/', + }, + ], + components: { + examples: {}, + headers: {}, + parameters: {}, + requestBodies: {}, + responses: {}, + schemas: { + JsonObject: { + type: 'object', + properties: {}, + additionalProperties: {}, + }, + }, + securitySchemes: { + JWT: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + paths: { + '/query': { + get: { + operationId: 'Query', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + results: { + type: 'array', + items: { + type: 'object', + properties: { + type: { + type: 'string', + description: 'The "type" of the given document.', + }, + document: { + type: 'object', + description: + 'The raw value of the document, as indexed.', + properties: { + title: { + type: 'string', + description: + 'The primary name of the document (e.g. name, title, identifier, etc).', + }, + text: { + type: 'string', + description: + 'Free-form text of the document (e.g. description, content, etc).', + }, + location: { + type: 'string', + description: + 'The relative or absolute URL of the document (target when a search result is clicked).', + }, + }, + }, + highlight: { + type: 'object', + description: + 'Optional result highlight. Useful for improving the search result\ndisplay/experience.', + }, + rank: { + type: 'integer', + description: + 'Optional result rank, where 1 is the first/top result returned. \nUseful for understanding search effectiveness in analytics.', + }, + }, + required: ['type', 'document'], + additionalProperties: false, + }, + }, + nextPageCursor: { + type: 'string', + }, + previousPageCursor: { + type: 'string', + }, + numberOfResults: { + type: 'integer', + }, + }, + required: ['results'], + }, + }, + }, + }, + '400': { + description: 'Bad request', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'object', + properties: { + message: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + name: 'term', + in: 'query', + required: false, + schema: { + type: 'string', + default: '', + }, + }, + { + name: 'filters', + in: 'query', + required: false, + style: 'deepObject', + explode: true, + schema: { + $ref: '#/components/schemas/JsonObject', + }, + }, + { + name: 'types', + in: 'query', + required: false, + schema: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + { + name: 'pageCursor', + in: 'query', + required: false, + schema: { + type: 'string', + }, + }, + { + name: 'pageLimit', + in: 'query', + required: false, + schema: { + type: 'integer', + }, + }, + ], + }, + }, + }, +} as const; diff --git a/plugins/search-backend/src/schema/openapi.yaml b/plugins/search-backend/src/schema/openapi.yaml new file mode 100644 index 0000000000..e7f44c9f11 --- /dev/null +++ b/plugins/search-backend/src/schema/openapi.yaml @@ -0,0 +1,135 @@ +openapi: 3.0.3 + +info: + title: '@backstage/plugin-search-backend' + version: '1' + description: The Backstage backend plugin that provides search functionality. + license: + name: Apache-2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + contact: {} + +servers: + - url: / + +components: + examples: {} + headers: {} + parameters: {} + requestBodies: {} + responses: {} + schemas: + JsonObject: + type: object + properties: {} + # Free form object. + additionalProperties: {} + securitySchemes: + JWT: + type: http + scheme: bearer + bearerFormat: JWT +paths: + /query: + get: + operationId: Query + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + type: object + properties: + type: + type: string + description: The "type" of the given document. + document: + type: object + description: The raw value of the document, as indexed. + properties: + title: + type: string + description: The primary name of the document (e.g. name, title, identifier, etc). + text: + type: string + description: Free-form text of the document (e.g. description, content, etc). + location: + type: string + description: The relative or absolute URL of the document (target when a search result is clicked). + highlight: + type: object + description: |- + Optional result highlight. Useful for improving the search result + display/experience. + rank: + type: integer + description: |- + Optional result rank, where 1 is the first/top result returned. + Useful for understanding search effectiveness in analytics. + required: + - type + - document + additionalProperties: false + nextPageCursor: + type: string + previousPageCursor: + type: string + numberOfResults: + type: integer + required: + - results + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: object + properties: + message: + type: string + security: + - {} + - JWT: [] + parameters: + - name: term + in: query + required: false + schema: + type: string + default: '' + - name: filters + in: query + required: false + style: deepObject + explode: true + schema: + # JsonObject is used here instead of the full ZOD schema definition as + # resolution of recursive schemas isn't possible with the library we're using + # and it causes a performance hit when _trying_ to resolve them. + $ref: '#/components/schemas/JsonObject' + - name: types + in: query + required: false + schema: + type: array + items: + type: string + - name: pageCursor + in: query + required: false + schema: + type: string + - name: pageLimit + in: query + required: false + schema: + type: integer diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 10f47d180f..3643eec38a 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -19,7 +19,7 @@ import Router from 'express-promise-router'; import { Logger } from 'winston'; import { z } from 'zod'; import { errorHandler } from '@backstage/backend-common'; -import { ErrorResponseBody, InputError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { JsonObject, JsonValue } from '@backstage/types'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; @@ -35,6 +35,8 @@ import { } from '@backstage/plugin-search-common'; import { SearchEngine } from '@backstage/plugin-search-common'; import { AuthorizedSearchEngine } from './AuthorizedSearchEngine'; +import type { ApiRouter } from '@backstage/backend-openapi-utils'; +import spec from '../schema/openapi.generated'; const jsonObjectSchema: z.ZodSchema = z.lazy(() => { const jsonValueSchema: z.ZodSchema = z.lazy(() => @@ -146,49 +148,43 @@ export async function createRouter( })), }); - const router = Router(); - router.get( - '/query', - async ( - req: express.Request, - res: express.Response, - ) => { - const parseResult = requestSchema.passthrough().safeParse(req.query); + const router = Router() as ApiRouter; + router.get('/query', async (req, res) => { + const parseResult = requestSchema.passthrough().safeParse(req.query); - if (!parseResult.success) { - throw new InputError(`Invalid query string: ${parseResult.error}`); + if (!parseResult.success) { + throw new InputError(`Invalid query string: ${parseResult.error}`); + } + + const query = parseResult.data; + + logger.info( + `Search request received: term="${query.term}", filters=${JSON.stringify( + query.filters, + )}, types=${query.types ? query.types.join(',') : ''}, pageCursor=${ + query.pageCursor ?? '' + }`, + ); + + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + try { + const resultSet = await engine?.query(query, { token }); + + res.json(filterResultSet(toSearchResults(resultSet))); + } catch (error) { + if (error.name === 'MissingIndexError') { + // re-throw and let the default error handler middleware captures it and serializes it with the right response code on the standard form + throw error; } - const query = parseResult.data; - - logger.info( - `Search request received: term="${ - query.term - }", filters=${JSON.stringify(query.filters)}, types=${ - query.types ? query.types.join(',') : '' - }, pageCursor=${query.pageCursor ?? ''}`, + throw new Error( + `There was a problem performing the search query: ${error.message}`, ); - - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); - - try { - const resultSet = await engine?.query(query, { token }); - - res.json(filterResultSet(toSearchResults(resultSet))); - } catch (error) { - if (error.name === 'MissingIndexError') { - // re-throw and let the default error handler middleware captures it and serializes it with the right response code on the standard form - throw error; - } - - throw new Error( - `There was a problem performing the search query: ${error.message}`, - ); - } - }, - ); + } + }); router.use(errorHandler()); diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 569e66eaba..411953c6f9 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-search-react +## 1.6.0-next.2 + +### Minor Changes + +- 1ce7f84b2e8: accepts InputProp property that can override keys from default + +### Patch Changes + +- adb31096bc2: Fix text-overflow UI issue for Lifecycle spans in SearchFilter checkbox labels. +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 1.6.0-next.1 ### Patch Changes diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index e39e91de72..8afde8c4a5 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -260,7 +260,7 @@ export type SearchPaginationLimitText = (params: { export type SearchPaginationProps = Omit< SearchPaginationBaseProps, | 'pageLimit' - | 'onPageLimitChange' + | 'onLimitChange' | 'pageCursor' | 'onPageCursorChange' | 'hasNextPage' diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 2467c60b09..e424788e31 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.6.0-next.1", + "version": "1.6.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx index 559ae1776e..7e97d790a7 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx @@ -92,13 +92,18 @@ const useStyles = makeStyles({ borderRadius: '50px', margin: 'auto', }, + notchedOutline: { + borderStyle: 'none', + }, }); export const CustomStyles = () => { const classes = useStyles(); return ( - + ); }; diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.tsx index 913f515aaa..cfd0538694 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.tsx @@ -80,7 +80,8 @@ export const SearchBarBase: ForwardRefExoticComponent = value: defaultValue, label, placeholder, - inputProps: defaultInputProps = {}, + inputProps = {}, + InputProps = {}, endAdornment, ...rest } = props; @@ -168,10 +169,11 @@ export const SearchBarBase: ForwardRefExoticComponent = endAdornment: clearButton ? clearButtonEndAdornment : endAdornment, + ...InputProps, }} inputProps={{ 'aria-label': ariaLabel, - ...defaultInputProps, + ...inputProps, }} fullWidth={fullWidth} onChange={handleChange} diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx index 78ef981ebc..6194ab8387 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx @@ -24,6 +24,7 @@ import { Select, MenuItem, FormLabel, + Typography, } from '@material-ui/core'; import { useSearch } from '../../context'; @@ -37,6 +38,16 @@ const useStyles = makeStyles({ label: { textTransform: 'capitalize', }, + checkboxWrapper: { + display: 'flex', + alignItems: 'center', + width: '100%', + }, + textWrapper: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, }); /** @@ -119,6 +130,11 @@ export const CheckboxFilter = (props: SearchFilterComponentProps) => { {values.map((value: string) => ( { checked={((filters[name] as string[]) ?? []).includes(value)} /> } - label={value} /> ))} @@ -197,7 +212,9 @@ export const SelectFilter = (props: SearchFilterComponentProps) => { {values.map((value: string) => ( - {value} + + {value} + ))} diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx index d2ecc55d31..65e65d9436 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx @@ -185,4 +185,31 @@ describe('SearchPagination', () => { }), ); }); + + it('Resets page cursor when page limit changes', async () => { + const initialState = { + term: '', + types: [], + filters: {}, + pageCursor: 'Mg==', // page: 2 + }; + + await renderWithEffects( + + + + + , + ); + + await userEvent.click(screen.getByText('25')); // first click to open the options + await userEvent.click(screen.getByText('10')); // second click to select the option + + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + pageCursor: undefined, + pageLimit: 10, + }), + ); + }); }); diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx index 7ab9e8604c..743d3ccb52 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx @@ -176,7 +176,7 @@ export const SearchPaginationBase = (props: SearchPaginationBaseProps) => { export type SearchPaginationProps = Omit< SearchPaginationBaseProps, | 'pageLimit' - | 'onPageLimitChange' + | 'onLimitChange' | 'pageCursor' | 'onPageCursorChange' | 'hasNextPage' @@ -191,12 +191,20 @@ export const SearchPagination = (props: SearchPaginationProps) => { const { pageLimit, setPageLimit, pageCursor, setPageCursor, fetchNextPage } = useSearch(); + const handlePageLimitChange = useCallback( + (newPageLimit: number) => { + setPageLimit(newPageLimit); + setPageCursor(undefined); + }, + [setPageLimit, setPageCursor], + ); + return ( diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index c5209d2859..f3c93f5142 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search +## 1.3.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 1.3.0-next.1 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index eccd5a8304..c4f7f680b6 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "1.3.0-next.1", + "version": "1.3.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 8fd1432c3a..be844d6c4f 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sentry +## 0.5.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.5.3-next.1 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index e83531f95a..70177fc4d7 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.5.3-next.1", + "version": "0.5.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index bbe2b74584..fab37db64c 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-shortcuts +## 0.3.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.10-next.1 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 4695c858c3..511247369f 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.3.10-next.1", + "version": "0.3.10-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 3a599bfd5f..1e186aa844 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sonarqube +## 0.6.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.6.7-next.1 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 566a5cf757..fcf247969b 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sonarqube", "description": "", - "version": "0.6.7-next.1", + "version": "0.6.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index 1fe8e00511..4f1741cefe 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-splunk-on-call +## 0.4.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.4.7-next.1 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 97204eed93..95293d447d 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-splunk-on-call", "description": "A Backstage plugin that integrates towards Splunk On-Call", - "version": "0.4.7-next.1", + "version": "0.4.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/stack-overflow/CHANGELOG.md b/plugins/stack-overflow/CHANGELOG.md index 4e70b57ce3..dad8fb3385 100644 --- a/plugins/stack-overflow/CHANGELOG.md +++ b/plugins/stack-overflow/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-stack-overflow +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home@0.5.2-next.2 + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index d30311eb61..2f966224d2 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow", - "version": "0.1.15-next.1", + "version": "0.1.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/stackstorm/CHANGELOG.md b/plugins/stackstorm/CHANGELOG.md index bf8fedd4c3..3763d9d295 100644 --- a/plugins/stackstorm/CHANGELOG.md +++ b/plugins/stackstorm/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-stackstorm +## 0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.2-next.0 ### Patch Changes diff --git a/plugins/stackstorm/package.json b/plugins/stackstorm/package.json index c9a9e7fc16..8787199746 100644 --- a/plugins/stackstorm/package.json +++ b/plugins/stackstorm/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-stackstorm", "description": "A Backstage plugin that integrates towards StackStorm", - "version": "0.1.2-next.0", + "version": "0.1.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md index a59782e2cc..23fb28deea 100644 --- a/plugins/tech-insights/CHANGELOG.md +++ b/plugins/tech-insights/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-tech-insights +## 0.3.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.3.10-next.1 ### Patch Changes diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index e8ead43ad6..4e2604a33a 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights", - "version": "0.3.10-next.1", + "version": "0.3.10-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index c28aa05ea9..c6d0bd29cd 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-tech-radar +## 0.6.4-next.2 + +### Patch Changes + +- be4fa53fab8: Fix description links when clicking entry in radar. +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.6.4-next.1 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 4621f89c87..a5f6456850 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-tech-radar", "description": "A Backstage plugin that lets you display a Tech Radar for your organization", - "version": "0.6.4-next.1", + "version": "0.6.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx index af9e840ead..66940cd1d7 100644 --- a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx +++ b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx @@ -26,6 +26,7 @@ export type Props = { value: number; color: string; url?: string; + links?: Array<{ title: string; url: string }>; moved?: number; description?: string; timeline?: EntrySnapshot[]; @@ -73,6 +74,7 @@ const RadarEntry = (props: Props): JSX.Element => { title, color, url, + links, value, x, y, @@ -112,6 +114,7 @@ const RadarEntry = (props: Props): JSX.Element => { description={description ? description : 'no description'} timeline={timeline ? timeline : []} url={url} + links={links} /> )} {description ? ( diff --git a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx index 957017692b..0fb6e3a36a 100644 --- a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx +++ b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx @@ -73,6 +73,7 @@ const RadarPlot = (props: Props): JSX.Element => { color={entry.color || ''} value={(entry?.index || 0) + 1} url={entry.url} + links={entry.links} description={entry.description} moved={entry.moved} title={entry.title} diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index cb41a62031..051190ab61 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/test-utils@1.3.1-next.2 + - @backstage/plugin-catalog@1.11.0-next.2 + - @backstage/plugin-techdocs@1.6.2-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 1.0.13-next.1 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index 27ae3ccadd..7de356960f 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.13-next.1", + "version": "1.0.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index 283f08934f..b267a89b8b 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 1.0.13-next.1 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 763696fbbf..527e5487dc 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-module-addons-contrib", "description": "Plugin module for contributed TechDocs Addons", - "version": "1.0.13-next.1", + "version": "1.0.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css b/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css index 63170792f1..3371d0fa53 100644 --- a/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css +++ b/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css @@ -5,3 +5,7 @@ color: rgba(0, 0, 0, 0.87); text-shadow: none; } + +.pswp__img { + cursor: pointer !important; +} diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 2513033dc3..dde49d6b2c 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -39,11 +39,11 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@aws-sdk/client-s3": "^3.208.0", - "@aws-sdk/credential-providers": "^3.208.0", - "@aws-sdk/lib-storage": "^3.208.0", - "@aws-sdk/node-http-handler": "^3.208.0", - "@aws-sdk/types": "^3.208.0", + "@aws-sdk/client-s3": "^3.310.0", + "@aws-sdk/credential-providers": "^3.310.0", + "@aws-sdk/lib-storage": "^3.310.0", + "@aws-sdk/node-http-handler": "^3.310.0", + "@aws-sdk/types": "^3.310.0", "@azure/identity": "^2.1.0", "@azure/storage-blob": "^12.5.0", "@backstage/backend-common": "workspace:^", diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index d201805fb5..5247e6906f 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-techdocs-react +## 1.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.1-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 1.1.6-next.0 ### Patch Changes diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index 318f8d56de..8a8c2c5cbc 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-react", "description": "Shared frontend utilities for TechDocs and Addons", - "version": "1.1.6-next.0", + "version": "1.1.6-next.1", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 31a3653f2e..f076a23b0b 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-techdocs +## 1.6.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + ## 1.6.2-next.1 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index d0a6e49d13..1004055c45 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "1.6.2-next.1", + "version": "1.6.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index d98a44f874..9385ec0ee6 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -45,6 +45,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-openapi-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", "msw": "^1.0.0", diff --git a/plugins/todo-backend/src/schema/openapi.generated.ts b/plugins/todo-backend/src/schema/openapi.generated.ts new file mode 100644 index 0000000000..54486a5615 --- /dev/null +++ b/plugins/todo-backend/src/schema/openapi.generated.ts @@ -0,0 +1,208 @@ +/* + * 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. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +export default { + openapi: '3.0.3', + info: { + title: '@backstage/plugin-todo-backend', + version: '1', + description: + 'The Backstage backend plugin that provides source code todo comment browsing.', + license: { + name: 'Apache-2.0', + url: 'http://www.apache.org/licenses/LICENSE-2.0.html', + }, + contact: {}, + }, + servers: [ + { + url: '/', + }, + ], + components: { + examples: {}, + headers: {}, + parameters: {}, + requestBodies: {}, + responses: {}, + schemas: { + TodoItem: { + type: 'object', + properties: { + text: { + type: 'string', + description: 'The contents of the TODO comment', + }, + tag: { + type: 'string', + description: 'The tag used, e.g. TODO, FIXME', + }, + author: { + type: 'string', + description: 'References author, if any', + }, + viewUrl: { + type: 'string', + description: 'URL used to view the file', + }, + lineNumber: { + type: 'integer', + description: 'The line number of the file that the TODO occurs at', + }, + repoFilePath: { + type: 'string', + description: + 'The path of the file containing the TODO within the repo', + }, + }, + required: ['text', 'tag'], + additionalProperties: false, + }, + }, + securitySchemes: { + JWT: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + paths: { + '/v1/todos': { + get: { + operationId: 'ListTodos', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + $ref: '#/components/schemas/TodoItem', + }, + }, + totalCount: { + type: 'integer', + }, + offset: { + type: 'integer', + }, + limit: { + type: 'integer', + }, + }, + required: ['items', 'totalCount', 'offset', 'limit'], + additionalProperties: false, + }, + }, + }, + }, + '400': { + description: 'Bad request', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'object', + properties: { + message: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + name: 'entity', + in: 'query', + required: true, + schema: { + type: 'string', + minLength: 1, + description: 'A reference to the entity to list TODO items for', + }, + }, + { + name: 'orderBy', + in: 'query', + required: false, + schema: { + type: 'string', + pattern: '^(text|tag|author|viewUrl|repoFilePath)=(asc|desc)$', + description: + 'The field and direction used to sort the listed TODO items', + }, + }, + { + name: 'filter', + in: 'query', + required: false, + schema: { + type: 'array', + description: + 'A list of filters used to narrow down the listed TODO items', + items: { + type: 'string', + pattern: '^(text|tag|author|viewUrl|repoFilePath)=.+$', + }, + }, + }, + { + name: 'offset', + in: 'query', + required: false, + schema: { + type: 'integer', + minimum: 0, + description: 'The offset at which to start listing TODO items', + }, + }, + { + name: 'limit', + in: 'query', + required: false, + schema: { + type: 'integer', + minimum: 1, + description: 'The number of TODO items to list', + }, + }, + ], + }, + }, + }, +} as const; diff --git a/plugins/todo-backend/src/schema/openapi.yaml b/plugins/todo-backend/src/schema/openapi.yaml new file mode 100644 index 0000000000..3fba129644 --- /dev/null +++ b/plugins/todo-backend/src/schema/openapi.yaml @@ -0,0 +1,132 @@ +openapi: 3.0.3 + +info: + title: '@backstage/plugin-todo-backend' + version: '1' + description: The Backstage backend plugin that provides source code todo comment browsing. + license: + name: Apache-2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + contact: {} + +servers: + - url: / + +components: + examples: {} + headers: {} + parameters: {} + requestBodies: {} + responses: {} + schemas: + TodoItem: + type: object + properties: + text: + type: string + description: The contents of the TODO comment + tag: + type: string + description: The tag used, e.g. TODO, FIXME + author: + type: string + description: References author, if any + viewUrl: + type: string + description: URL used to view the file + lineNumber: + type: integer + description: The line number of the file that the TODO occurs at + repoFilePath: + type: string + description: The path of the file containing the TODO within the repo + required: + - text + - tag + additionalProperties: false + securitySchemes: + JWT: + type: http + scheme: bearer + bearerFormat: JWT +paths: + /v1/todos: + get: + operationId: ListTodos + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/TodoItem' + totalCount: + type: integer + offset: + type: integer + limit: + type: integer + required: + - items + - totalCount + - offset + - limit + additionalProperties: false + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: object + properties: + message: + type: string + security: + - {} + - JWT: [] + parameters: + - name: entity + in: query + required: true + schema: + type: string + minLength: 1 + description: A reference to the entity to list TODO items for + - name: orderBy + in: query + required: false + schema: + type: string + pattern: '^(text|tag|author|viewUrl|repoFilePath)=(asc|desc)$' + description: The field and direction used to sort the listed TODO items + - name: filter + in: query + required: false + schema: + type: array + description: A list of filters used to narrow down the listed TODO items + items: + type: string + pattern: '^(text|tag|author|viewUrl|repoFilePath)=.+$' + - name: offset + in: query + required: false + schema: + type: integer + minimum: 0 + description: The offset at which to start listing TODO items + - name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + description: The number of TODO items to list diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index 5954fab009..364cbef6b0 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -25,6 +25,8 @@ import { parseIntegerParam, parseOrderByParam, } from '../lib/utils'; +import spec from '../schema/openapi.generated'; +import type { ApiRouter } from '@backstage/backend-openapi-utils'; /** @public */ export interface RouterOptions { @@ -37,7 +39,7 @@ export async function createRouter( ): Promise { const { todoService } = options; - const router = Router(); + const router = Router() as ApiRouter; router.use(express.json()); router.get('/v1/todos', async (req, res) => { diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md index cb9e1464ff..0a0a205d06 100644 --- a/plugins/todo/CHANGELOG.md +++ b/plugins/todo/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-todo +## 0.2.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.20-next.1 ### Patch Changes diff --git a/plugins/todo/package.json b/plugins/todo/package.json index f8a86b9cb2..3dd5822154 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo", "description": "A Backstage plugin that lets you browse TODO comments in your source code", - "version": "0.2.20-next.1", + "version": "0.2.20-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 92558dfba7..5fd080fe60 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-user-settings +## 0.7.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-app-api@1.8.0-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.7.3-next.1 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index b55293d414..98d688bcda 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings", "description": "A Backstage plugin that provides a settings page", - "version": "0.7.3-next.1", + "version": "0.7.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -78,7 +78,8 @@ "type": "object", "additionalProperties": { "type": "object", - "visibility": "frontend" + "visibility": "frontend", + "additionalProperties": true } } } diff --git a/plugins/vault/CHANGELOG.md b/plugins/vault/CHANGELOG.md index 7af7f47adc..e5b521cdf9 100644 --- a/plugins/vault/CHANGELOG.md +++ b/plugins/vault/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-vault +## 0.1.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/core-plugin-api@1.5.1 + ## 0.1.12-next.1 ### Patch Changes diff --git a/plugins/vault/package.json b/plugins/vault/package.json index 6d97b0a5e9..813217876c 100644 --- a/plugins/vault/package.json +++ b/plugins/vault/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-vault", "description": "A Backstage plugin that integrates towards Vault", - "version": "0.1.12-next.1", + "version": "0.1.12-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index eccce54f0b..fbd75b686f 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-xcmetrics +## 0.2.38-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/core-plugin-api@1.5.1 + ## 0.2.38-next.0 ### Patch Changes diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 18b86eb0ee..54dea03612 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-xcmetrics", "description": "A Backstage plugin that shows XCode build metrics for your components", - "version": "0.2.38-next.0", + "version": "0.2.38-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx b/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx index 5062da4749..bc81b57882 100644 --- a/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx +++ b/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx @@ -18,7 +18,7 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { BuildListFilter } from './BuildListFilter'; import { BuildFilters, xcmetricsApiRef } from '../../api'; -import { RenderResult } from '@testing-library/react'; +import { RenderResult, within } from '@testing-library/react'; jest.mock('../../api/XcmetricsClient'); const client = require('../../api/XcmetricsClient'); @@ -50,13 +50,13 @@ const renderWithFiltersVisible = async ( const setStatusFilter = async (rendered: RenderResult, option: string) => { const statusSelect = rendered.getAllByTestId('select')[0]; - await userEvent.click(statusSelect); + await userEvent.click(within(statusSelect).getByRole('button')); await userEvent.click((await rendered.findAllByText(option))[0]); }; const setProjectFilter = async (rendered: RenderResult, option: string) => { const statusSelect = rendered.getAllByTestId('select')[1]; - await userEvent.click(statusSelect); + await userEvent.click(within(statusSelect).getByRole('button')); const options = await rendered.findAllByText(option); await userEvent.click(options[options.length - 1]); }; diff --git a/yarn.lock b/yarn.lock index 0bd97d81f8..82626c846e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -85,8 +85,8 @@ __metadata: linkType: hard "@apollo/client@npm:^3.0.0": - version: 3.7.13 - resolution: "@apollo/client@npm:3.7.13" + version: 3.7.14 + resolution: "@apollo/client@npm:3.7.14" dependencies: "@graphql-typed-document-node/core": ^3.1.1 "@wry/context": ^0.7.0 @@ -116,7 +116,7 @@ __metadata: optional: true subscriptions-transport-ws: optional: true - checksum: 9c8adfcb5da0ec8adaa1f7bc7c16bc6fa4e5ec171422ca985043d92e1012958f0ab9b1bcb5a25167c713995f0502f0240a807114941d48b118de34dbd9f63428 + checksum: eaa8af74450fee4d4761d25fb8fe752788517b27ebb46a51b6496950c5b73f48b6ba3e4f3474b6e4a4f310916dc3fcf079306c35f16d4284b96c7fdbb8539fde languageName: node linkType: hard @@ -182,8 +182,8 @@ __metadata: linkType: hard "@apollo/server@npm:^4.0.0": - version: 4.7.0 - resolution: "@apollo/server@npm:4.7.0" + version: 4.7.1 + resolution: "@apollo/server@npm:4.7.1" dependencies: "@apollo/cache-control-types": ^1.0.2 "@apollo/server-gateway-interface": ^1.1.0 @@ -213,7 +213,7 @@ __metadata: whatwg-mimetype: ^3.0.0 peerDependencies: graphql: ^16.6.0 - checksum: 68f94a3859b99b931d8c6ae36f8684ec85eb7524a74e2a814b36ae3ae87773ff73562657021eaa2c2d382ec32bb6b1d9f9f7975fdc0a4509f24cddd71d33953e + checksum: 6c8bdd240a4c688641bfec0520375db0eb78d2495286f30f962d7b6e97d7db70f105c09364e56103e1b93543e5760621666655eaa8ae7218312ac54ca7200d4b languageName: node linkType: hard @@ -531,13 +531,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/abort-controller@npm:3.310.0, @aws-sdk/abort-controller@npm:^3.208.0": - version: 3.310.0 - resolution: "@aws-sdk/abort-controller@npm:3.310.0" +"@aws-sdk/abort-controller@npm:3.329.0, @aws-sdk/abort-controller@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/abort-controller@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: ca081fbec7419ff7bc03b7fddcfe7be1d5e390290c819069f42672f5a66415b55d90a81de899c384fd3368396390e3573c66345a8f91683e600c6caff64a239f + checksum: 37f65402b2599d19793a2fc9de18d61eed4663fd8b565f2782bc5ab5b9bcf0adc4600275dca3860d66dbcf1f347e5c506ca2cb41842e9f64d625cad9b4676fef languageName: node linkType: hard @@ -550,532 +550,620 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-cognito-identity@npm:3.312.0": - version: 3.312.0 - resolution: "@aws-sdk/client-cognito-identity@npm:3.312.0" +"@aws-sdk/client-cognito-identity@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.329.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.312.0 - "@aws-sdk/config-resolver": 3.310.0 - "@aws-sdk/credential-provider-node": 3.310.0 - "@aws-sdk/fetch-http-handler": 3.310.0 - "@aws-sdk/hash-node": 3.310.0 - "@aws-sdk/invalid-dependency": 3.310.0 - "@aws-sdk/middleware-content-length": 3.310.0 - "@aws-sdk/middleware-endpoint": 3.310.0 - "@aws-sdk/middleware-host-header": 3.310.0 - "@aws-sdk/middleware-logger": 3.310.0 - "@aws-sdk/middleware-recursion-detection": 3.310.0 - "@aws-sdk/middleware-retry": 3.310.0 - "@aws-sdk/middleware-serde": 3.310.0 - "@aws-sdk/middleware-signing": 3.310.0 - "@aws-sdk/middleware-stack": 3.310.0 - "@aws-sdk/middleware-user-agent": 3.310.0 - "@aws-sdk/node-config-provider": 3.310.0 - "@aws-sdk/node-http-handler": 3.310.0 - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/smithy-client": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/url-parser": 3.310.0 + "@aws-sdk/client-sts": 3.329.0 + "@aws-sdk/config-resolver": 3.329.0 + "@aws-sdk/credential-provider-node": 3.329.0 + "@aws-sdk/fetch-http-handler": 3.329.0 + "@aws-sdk/hash-node": 3.329.0 + "@aws-sdk/invalid-dependency": 3.329.0 + "@aws-sdk/middleware-content-length": 3.329.0 + "@aws-sdk/middleware-endpoint": 3.329.0 + "@aws-sdk/middleware-host-header": 3.329.0 + "@aws-sdk/middleware-logger": 3.329.0 + "@aws-sdk/middleware-recursion-detection": 3.329.0 + "@aws-sdk/middleware-retry": 3.329.0 + "@aws-sdk/middleware-serde": 3.329.0 + "@aws-sdk/middleware-signing": 3.329.0 + "@aws-sdk/middleware-stack": 3.329.0 + "@aws-sdk/middleware-user-agent": 3.329.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/node-http-handler": 3.329.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/smithy-client": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/url-parser": 3.329.0 "@aws-sdk/util-base64": 3.310.0 "@aws-sdk/util-body-length-browser": 3.310.0 "@aws-sdk/util-body-length-node": 3.310.0 - "@aws-sdk/util-defaults-mode-browser": 3.310.0 - "@aws-sdk/util-defaults-mode-node": 3.310.0 - "@aws-sdk/util-endpoints": 3.310.0 - "@aws-sdk/util-retry": 3.310.0 - "@aws-sdk/util-user-agent-browser": 3.310.0 - "@aws-sdk/util-user-agent-node": 3.310.0 + "@aws-sdk/util-defaults-mode-browser": 3.329.0 + "@aws-sdk/util-defaults-mode-node": 3.329.0 + "@aws-sdk/util-endpoints": 3.329.0 + "@aws-sdk/util-retry": 3.329.0 + "@aws-sdk/util-user-agent-browser": 3.329.0 + "@aws-sdk/util-user-agent-node": 3.329.0 "@aws-sdk/util-utf8": 3.310.0 tslib: ^2.5.0 - checksum: 1bbd307c899e112bc265927e052afebe7e25d111fce8e765c85c40a84152910549dd25d9d8f4cb0561773f9be72c5a71f0ff9577582f61ee30e27d4188dc06d5 + checksum: bb60cbad68375fda44f7a4cd538feb55ed744a8cf510c56f8fe71c7611fef75b6b7d4bf6ff2190283b5bedf184f4cb13d6867ccebbf8588e6176de09133c9e52 languageName: node linkType: hard -"@aws-sdk/client-s3@npm:^3.208.0": - version: 3.312.0 - resolution: "@aws-sdk/client-s3@npm:3.312.0" +"@aws-sdk/client-eks@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/client-eks@npm:3.329.0" + dependencies: + "@aws-crypto/sha256-browser": 3.0.0 + "@aws-crypto/sha256-js": 3.0.0 + "@aws-sdk/client-sts": 3.329.0 + "@aws-sdk/config-resolver": 3.329.0 + "@aws-sdk/credential-provider-node": 3.329.0 + "@aws-sdk/fetch-http-handler": 3.329.0 + "@aws-sdk/hash-node": 3.329.0 + "@aws-sdk/invalid-dependency": 3.329.0 + "@aws-sdk/middleware-content-length": 3.329.0 + "@aws-sdk/middleware-endpoint": 3.329.0 + "@aws-sdk/middleware-host-header": 3.329.0 + "@aws-sdk/middleware-logger": 3.329.0 + "@aws-sdk/middleware-recursion-detection": 3.329.0 + "@aws-sdk/middleware-retry": 3.329.0 + "@aws-sdk/middleware-serde": 3.329.0 + "@aws-sdk/middleware-signing": 3.329.0 + "@aws-sdk/middleware-stack": 3.329.0 + "@aws-sdk/middleware-user-agent": 3.329.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/node-http-handler": 3.329.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/smithy-client": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/url-parser": 3.329.0 + "@aws-sdk/util-base64": 3.310.0 + "@aws-sdk/util-body-length-browser": 3.310.0 + "@aws-sdk/util-body-length-node": 3.310.0 + "@aws-sdk/util-defaults-mode-browser": 3.329.0 + "@aws-sdk/util-defaults-mode-node": 3.329.0 + "@aws-sdk/util-endpoints": 3.329.0 + "@aws-sdk/util-retry": 3.329.0 + "@aws-sdk/util-user-agent-browser": 3.329.0 + "@aws-sdk/util-user-agent-node": 3.329.0 + "@aws-sdk/util-utf8": 3.310.0 + "@aws-sdk/util-waiter": 3.329.0 + tslib: ^2.5.0 + uuid: ^8.3.2 + checksum: 373176c8cc23b3aef3f1b1088568b2ec1d635597119fe8d2633850a7edb214c14e8990665045c70658076b27413aa8915f04946ab13c0e15eb1041a1954e2f1e + languageName: node + linkType: hard + +"@aws-sdk/client-organizations@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/client-organizations@npm:3.329.0" + dependencies: + "@aws-crypto/sha256-browser": 3.0.0 + "@aws-crypto/sha256-js": 3.0.0 + "@aws-sdk/client-sts": 3.329.0 + "@aws-sdk/config-resolver": 3.329.0 + "@aws-sdk/credential-provider-node": 3.329.0 + "@aws-sdk/fetch-http-handler": 3.329.0 + "@aws-sdk/hash-node": 3.329.0 + "@aws-sdk/invalid-dependency": 3.329.0 + "@aws-sdk/middleware-content-length": 3.329.0 + "@aws-sdk/middleware-endpoint": 3.329.0 + "@aws-sdk/middleware-host-header": 3.329.0 + "@aws-sdk/middleware-logger": 3.329.0 + "@aws-sdk/middleware-recursion-detection": 3.329.0 + "@aws-sdk/middleware-retry": 3.329.0 + "@aws-sdk/middleware-serde": 3.329.0 + "@aws-sdk/middleware-signing": 3.329.0 + "@aws-sdk/middleware-stack": 3.329.0 + "@aws-sdk/middleware-user-agent": 3.329.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/node-http-handler": 3.329.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/smithy-client": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/url-parser": 3.329.0 + "@aws-sdk/util-base64": 3.310.0 + "@aws-sdk/util-body-length-browser": 3.310.0 + "@aws-sdk/util-body-length-node": 3.310.0 + "@aws-sdk/util-defaults-mode-browser": 3.329.0 + "@aws-sdk/util-defaults-mode-node": 3.329.0 + "@aws-sdk/util-endpoints": 3.329.0 + "@aws-sdk/util-retry": 3.329.0 + "@aws-sdk/util-user-agent-browser": 3.329.0 + "@aws-sdk/util-user-agent-node": 3.329.0 + "@aws-sdk/util-utf8": 3.310.0 + tslib: ^2.5.0 + checksum: d36b5d05005a3603153c11f1b0fd4272fe5c5fb3af7209c9788d15731016e635c1ae45bfdcbd0efe69c4b3e197dc6ee31e1df708ecfafa13f04742eaf444a670 + languageName: node + linkType: hard + +"@aws-sdk/client-s3@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/client-s3@npm:3.329.0" dependencies: "@aws-crypto/sha1-browser": 3.0.0 "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.312.0 - "@aws-sdk/config-resolver": 3.310.0 - "@aws-sdk/credential-provider-node": 3.310.0 - "@aws-sdk/eventstream-serde-browser": 3.310.0 - "@aws-sdk/eventstream-serde-config-resolver": 3.310.0 - "@aws-sdk/eventstream-serde-node": 3.310.0 - "@aws-sdk/fetch-http-handler": 3.310.0 - "@aws-sdk/hash-blob-browser": 3.310.0 - "@aws-sdk/hash-node": 3.310.0 - "@aws-sdk/hash-stream-node": 3.310.0 - "@aws-sdk/invalid-dependency": 3.310.0 - "@aws-sdk/md5-js": 3.310.0 - "@aws-sdk/middleware-bucket-endpoint": 3.310.0 - "@aws-sdk/middleware-content-length": 3.310.0 - "@aws-sdk/middleware-endpoint": 3.310.0 - "@aws-sdk/middleware-expect-continue": 3.310.0 - "@aws-sdk/middleware-flexible-checksums": 3.310.0 - "@aws-sdk/middleware-host-header": 3.310.0 - "@aws-sdk/middleware-location-constraint": 3.310.0 - "@aws-sdk/middleware-logger": 3.310.0 - "@aws-sdk/middleware-recursion-detection": 3.310.0 - "@aws-sdk/middleware-retry": 3.310.0 - "@aws-sdk/middleware-sdk-s3": 3.310.0 - "@aws-sdk/middleware-serde": 3.310.0 - "@aws-sdk/middleware-signing": 3.310.0 - "@aws-sdk/middleware-ssec": 3.310.0 - "@aws-sdk/middleware-stack": 3.310.0 - "@aws-sdk/middleware-user-agent": 3.310.0 - "@aws-sdk/node-config-provider": 3.310.0 - "@aws-sdk/node-http-handler": 3.310.0 - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/signature-v4-multi-region": 3.310.0 - "@aws-sdk/smithy-client": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/url-parser": 3.310.0 + "@aws-sdk/client-sts": 3.329.0 + "@aws-sdk/config-resolver": 3.329.0 + "@aws-sdk/credential-provider-node": 3.329.0 + "@aws-sdk/eventstream-serde-browser": 3.329.0 + "@aws-sdk/eventstream-serde-config-resolver": 3.329.0 + "@aws-sdk/eventstream-serde-node": 3.329.0 + "@aws-sdk/fetch-http-handler": 3.329.0 + "@aws-sdk/hash-blob-browser": 3.329.0 + "@aws-sdk/hash-node": 3.329.0 + "@aws-sdk/hash-stream-node": 3.329.0 + "@aws-sdk/invalid-dependency": 3.329.0 + "@aws-sdk/md5-js": 3.329.0 + "@aws-sdk/middleware-bucket-endpoint": 3.329.0 + "@aws-sdk/middleware-content-length": 3.329.0 + "@aws-sdk/middleware-endpoint": 3.329.0 + "@aws-sdk/middleware-expect-continue": 3.329.0 + "@aws-sdk/middleware-flexible-checksums": 3.329.0 + "@aws-sdk/middleware-host-header": 3.329.0 + "@aws-sdk/middleware-location-constraint": 3.329.0 + "@aws-sdk/middleware-logger": 3.329.0 + "@aws-sdk/middleware-recursion-detection": 3.329.0 + "@aws-sdk/middleware-retry": 3.329.0 + "@aws-sdk/middleware-sdk-s3": 3.329.0 + "@aws-sdk/middleware-serde": 3.329.0 + "@aws-sdk/middleware-signing": 3.329.0 + "@aws-sdk/middleware-ssec": 3.329.0 + "@aws-sdk/middleware-stack": 3.329.0 + "@aws-sdk/middleware-user-agent": 3.329.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/node-http-handler": 3.329.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/signature-v4-multi-region": 3.329.0 + "@aws-sdk/smithy-client": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/url-parser": 3.329.0 "@aws-sdk/util-base64": 3.310.0 "@aws-sdk/util-body-length-browser": 3.310.0 "@aws-sdk/util-body-length-node": 3.310.0 - "@aws-sdk/util-defaults-mode-browser": 3.310.0 - "@aws-sdk/util-defaults-mode-node": 3.310.0 - "@aws-sdk/util-endpoints": 3.310.0 - "@aws-sdk/util-retry": 3.310.0 - "@aws-sdk/util-stream-browser": 3.310.0 - "@aws-sdk/util-stream-node": 3.310.0 - "@aws-sdk/util-user-agent-browser": 3.310.0 - "@aws-sdk/util-user-agent-node": 3.310.0 + "@aws-sdk/util-defaults-mode-browser": 3.329.0 + "@aws-sdk/util-defaults-mode-node": 3.329.0 + "@aws-sdk/util-endpoints": 3.329.0 + "@aws-sdk/util-retry": 3.329.0 + "@aws-sdk/util-stream-browser": 3.329.0 + "@aws-sdk/util-stream-node": 3.329.0 + "@aws-sdk/util-user-agent-browser": 3.329.0 + "@aws-sdk/util-user-agent-node": 3.329.0 "@aws-sdk/util-utf8": 3.310.0 - "@aws-sdk/util-waiter": 3.310.0 + "@aws-sdk/util-waiter": 3.329.0 "@aws-sdk/xml-builder": 3.310.0 fast-xml-parser: 4.1.2 tslib: ^2.5.0 - checksum: 20764e30d10a0280961ba45491f6b26f7ca27e430da348fa4ce257b75fe1b67d5dbf0f4e3109c9db33c8c3269ba1f62d4a2d53a45977e1271dbc189bfcc1d4ef + checksum: dd413da8c16a0ed2246e60a65c18dad143535e72f234da35ba85561dc14ec06e669f155bf37039c9dc7a15547bc6ce792d9fec89d7b0a896b37aa54ebb2ae16c languageName: node linkType: hard -"@aws-sdk/client-sqs@npm:^3.208.0": - version: 3.312.0 - resolution: "@aws-sdk/client-sqs@npm:3.312.0" +"@aws-sdk/client-sqs@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/client-sqs@npm:3.329.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.312.0 - "@aws-sdk/config-resolver": 3.310.0 - "@aws-sdk/credential-provider-node": 3.310.0 - "@aws-sdk/fetch-http-handler": 3.310.0 - "@aws-sdk/hash-node": 3.310.0 - "@aws-sdk/invalid-dependency": 3.310.0 - "@aws-sdk/md5-js": 3.310.0 - "@aws-sdk/middleware-content-length": 3.310.0 - "@aws-sdk/middleware-endpoint": 3.310.0 - "@aws-sdk/middleware-host-header": 3.310.0 - "@aws-sdk/middleware-logger": 3.310.0 - "@aws-sdk/middleware-recursion-detection": 3.310.0 - "@aws-sdk/middleware-retry": 3.310.0 - "@aws-sdk/middleware-sdk-sqs": 3.310.0 - "@aws-sdk/middleware-serde": 3.310.0 - "@aws-sdk/middleware-signing": 3.310.0 - "@aws-sdk/middleware-stack": 3.310.0 - "@aws-sdk/middleware-user-agent": 3.310.0 - "@aws-sdk/node-config-provider": 3.310.0 - "@aws-sdk/node-http-handler": 3.310.0 - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/smithy-client": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/url-parser": 3.310.0 + "@aws-sdk/client-sts": 3.329.0 + "@aws-sdk/config-resolver": 3.329.0 + "@aws-sdk/credential-provider-node": 3.329.0 + "@aws-sdk/fetch-http-handler": 3.329.0 + "@aws-sdk/hash-node": 3.329.0 + "@aws-sdk/invalid-dependency": 3.329.0 + "@aws-sdk/md5-js": 3.329.0 + "@aws-sdk/middleware-content-length": 3.329.0 + "@aws-sdk/middleware-endpoint": 3.329.0 + "@aws-sdk/middleware-host-header": 3.329.0 + "@aws-sdk/middleware-logger": 3.329.0 + "@aws-sdk/middleware-recursion-detection": 3.329.0 + "@aws-sdk/middleware-retry": 3.329.0 + "@aws-sdk/middleware-sdk-sqs": 3.329.0 + "@aws-sdk/middleware-serde": 3.329.0 + "@aws-sdk/middleware-signing": 3.329.0 + "@aws-sdk/middleware-stack": 3.329.0 + "@aws-sdk/middleware-user-agent": 3.329.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/node-http-handler": 3.329.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/smithy-client": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/url-parser": 3.329.0 "@aws-sdk/util-base64": 3.310.0 "@aws-sdk/util-body-length-browser": 3.310.0 "@aws-sdk/util-body-length-node": 3.310.0 - "@aws-sdk/util-defaults-mode-browser": 3.310.0 - "@aws-sdk/util-defaults-mode-node": 3.310.0 - "@aws-sdk/util-endpoints": 3.310.0 - "@aws-sdk/util-retry": 3.310.0 - "@aws-sdk/util-user-agent-browser": 3.310.0 - "@aws-sdk/util-user-agent-node": 3.310.0 + "@aws-sdk/util-defaults-mode-browser": 3.329.0 + "@aws-sdk/util-defaults-mode-node": 3.329.0 + "@aws-sdk/util-endpoints": 3.329.0 + "@aws-sdk/util-retry": 3.329.0 + "@aws-sdk/util-user-agent-browser": 3.329.0 + "@aws-sdk/util-user-agent-node": 3.329.0 "@aws-sdk/util-utf8": 3.310.0 fast-xml-parser: 4.1.2 tslib: ^2.5.0 - checksum: 0cc2c0cb6b835d5dd29ff0f146fd4f7d4d7501ebdd419144a704a6092a029e774f580a6dcb6e668779fe86c589b6df884ef09f1866d3d28abbeb1787fe7eb58e + checksum: 10559224f1d77ed6ddef93fd1b91518e5b24b0c75a71287cc5b3e9000424af1b253a17ad1bc852c48849cecd735b99aed916f04370d0f3755ba3b65bd45a478c languageName: node linkType: hard -"@aws-sdk/client-sso-oidc@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/client-sso-oidc@npm:3.310.0" +"@aws-sdk/client-sso-oidc@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/client-sso-oidc@npm:3.329.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/config-resolver": 3.310.0 - "@aws-sdk/fetch-http-handler": 3.310.0 - "@aws-sdk/hash-node": 3.310.0 - "@aws-sdk/invalid-dependency": 3.310.0 - "@aws-sdk/middleware-content-length": 3.310.0 - "@aws-sdk/middleware-endpoint": 3.310.0 - "@aws-sdk/middleware-host-header": 3.310.0 - "@aws-sdk/middleware-logger": 3.310.0 - "@aws-sdk/middleware-recursion-detection": 3.310.0 - "@aws-sdk/middleware-retry": 3.310.0 - "@aws-sdk/middleware-serde": 3.310.0 - "@aws-sdk/middleware-stack": 3.310.0 - "@aws-sdk/middleware-user-agent": 3.310.0 - "@aws-sdk/node-config-provider": 3.310.0 - "@aws-sdk/node-http-handler": 3.310.0 - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/smithy-client": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/url-parser": 3.310.0 + "@aws-sdk/config-resolver": 3.329.0 + "@aws-sdk/fetch-http-handler": 3.329.0 + "@aws-sdk/hash-node": 3.329.0 + "@aws-sdk/invalid-dependency": 3.329.0 + "@aws-sdk/middleware-content-length": 3.329.0 + "@aws-sdk/middleware-endpoint": 3.329.0 + "@aws-sdk/middleware-host-header": 3.329.0 + "@aws-sdk/middleware-logger": 3.329.0 + "@aws-sdk/middleware-recursion-detection": 3.329.0 + "@aws-sdk/middleware-retry": 3.329.0 + "@aws-sdk/middleware-serde": 3.329.0 + "@aws-sdk/middleware-stack": 3.329.0 + "@aws-sdk/middleware-user-agent": 3.329.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/node-http-handler": 3.329.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/smithy-client": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/url-parser": 3.329.0 "@aws-sdk/util-base64": 3.310.0 "@aws-sdk/util-body-length-browser": 3.310.0 "@aws-sdk/util-body-length-node": 3.310.0 - "@aws-sdk/util-defaults-mode-browser": 3.310.0 - "@aws-sdk/util-defaults-mode-node": 3.310.0 - "@aws-sdk/util-endpoints": 3.310.0 - "@aws-sdk/util-retry": 3.310.0 - "@aws-sdk/util-user-agent-browser": 3.310.0 - "@aws-sdk/util-user-agent-node": 3.310.0 + "@aws-sdk/util-defaults-mode-browser": 3.329.0 + "@aws-sdk/util-defaults-mode-node": 3.329.0 + "@aws-sdk/util-endpoints": 3.329.0 + "@aws-sdk/util-retry": 3.329.0 + "@aws-sdk/util-user-agent-browser": 3.329.0 + "@aws-sdk/util-user-agent-node": 3.329.0 "@aws-sdk/util-utf8": 3.310.0 tslib: ^2.5.0 - checksum: 7e4a013983978a02e390b621d76716c1649eb7b8889ad239f082b737ef204182879c4ab735a702ebd9c9a5a406c1fefd16c53d7f327db2cbaea88ebbf9bc6123 + checksum: 6ddb767778548b4e6952a0efc867fa53c37b37149d3b762577db80c1d3b51a5c9a92e85fb34affd91c73e64b55a9e9184bf63b1301f3bd5034daa7effd645dc6 languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/client-sso@npm:3.310.0" +"@aws-sdk/client-sso@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/client-sso@npm:3.329.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/config-resolver": 3.310.0 - "@aws-sdk/fetch-http-handler": 3.310.0 - "@aws-sdk/hash-node": 3.310.0 - "@aws-sdk/invalid-dependency": 3.310.0 - "@aws-sdk/middleware-content-length": 3.310.0 - "@aws-sdk/middleware-endpoint": 3.310.0 - "@aws-sdk/middleware-host-header": 3.310.0 - "@aws-sdk/middleware-logger": 3.310.0 - "@aws-sdk/middleware-recursion-detection": 3.310.0 - "@aws-sdk/middleware-retry": 3.310.0 - "@aws-sdk/middleware-serde": 3.310.0 - "@aws-sdk/middleware-stack": 3.310.0 - "@aws-sdk/middleware-user-agent": 3.310.0 - "@aws-sdk/node-config-provider": 3.310.0 - "@aws-sdk/node-http-handler": 3.310.0 - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/smithy-client": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/url-parser": 3.310.0 + "@aws-sdk/config-resolver": 3.329.0 + "@aws-sdk/fetch-http-handler": 3.329.0 + "@aws-sdk/hash-node": 3.329.0 + "@aws-sdk/invalid-dependency": 3.329.0 + "@aws-sdk/middleware-content-length": 3.329.0 + "@aws-sdk/middleware-endpoint": 3.329.0 + "@aws-sdk/middleware-host-header": 3.329.0 + "@aws-sdk/middleware-logger": 3.329.0 + "@aws-sdk/middleware-recursion-detection": 3.329.0 + "@aws-sdk/middleware-retry": 3.329.0 + "@aws-sdk/middleware-serde": 3.329.0 + "@aws-sdk/middleware-stack": 3.329.0 + "@aws-sdk/middleware-user-agent": 3.329.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/node-http-handler": 3.329.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/smithy-client": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/url-parser": 3.329.0 "@aws-sdk/util-base64": 3.310.0 "@aws-sdk/util-body-length-browser": 3.310.0 "@aws-sdk/util-body-length-node": 3.310.0 - "@aws-sdk/util-defaults-mode-browser": 3.310.0 - "@aws-sdk/util-defaults-mode-node": 3.310.0 - "@aws-sdk/util-endpoints": 3.310.0 - "@aws-sdk/util-retry": 3.310.0 - "@aws-sdk/util-user-agent-browser": 3.310.0 - "@aws-sdk/util-user-agent-node": 3.310.0 + "@aws-sdk/util-defaults-mode-browser": 3.329.0 + "@aws-sdk/util-defaults-mode-node": 3.329.0 + "@aws-sdk/util-endpoints": 3.329.0 + "@aws-sdk/util-retry": 3.329.0 + "@aws-sdk/util-user-agent-browser": 3.329.0 + "@aws-sdk/util-user-agent-node": 3.329.0 "@aws-sdk/util-utf8": 3.310.0 tslib: ^2.5.0 - checksum: 5d7461e2102634c336e3748f91f51e9e53c8dc2fa50441535fad8f6926f368131224935b958b05a071d9c61357adfc8db885ac2adb5be9cee94243cecc3454d3 + checksum: 27227bdefc73d6c5501418c752d4015217ac787c063bccf805aba2c213fb7abcc10862cb2faab5cfa7a36e29eeacb59aea61717cbb8aaf075cd69962bab3abc9 languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.312.0, @aws-sdk/client-sts@npm:^3.208.0": - version: 3.312.0 - resolution: "@aws-sdk/client-sts@npm:3.312.0" +"@aws-sdk/client-sts@npm:3.329.0, @aws-sdk/client-sts@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/client-sts@npm:3.329.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/config-resolver": 3.310.0 - "@aws-sdk/credential-provider-node": 3.310.0 - "@aws-sdk/fetch-http-handler": 3.310.0 - "@aws-sdk/hash-node": 3.310.0 - "@aws-sdk/invalid-dependency": 3.310.0 - "@aws-sdk/middleware-content-length": 3.310.0 - "@aws-sdk/middleware-endpoint": 3.310.0 - "@aws-sdk/middleware-host-header": 3.310.0 - "@aws-sdk/middleware-logger": 3.310.0 - "@aws-sdk/middleware-recursion-detection": 3.310.0 - "@aws-sdk/middleware-retry": 3.310.0 - "@aws-sdk/middleware-sdk-sts": 3.310.0 - "@aws-sdk/middleware-serde": 3.310.0 - "@aws-sdk/middleware-signing": 3.310.0 - "@aws-sdk/middleware-stack": 3.310.0 - "@aws-sdk/middleware-user-agent": 3.310.0 - "@aws-sdk/node-config-provider": 3.310.0 - "@aws-sdk/node-http-handler": 3.310.0 - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/smithy-client": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/url-parser": 3.310.0 + "@aws-sdk/config-resolver": 3.329.0 + "@aws-sdk/credential-provider-node": 3.329.0 + "@aws-sdk/fetch-http-handler": 3.329.0 + "@aws-sdk/hash-node": 3.329.0 + "@aws-sdk/invalid-dependency": 3.329.0 + "@aws-sdk/middleware-content-length": 3.329.0 + "@aws-sdk/middleware-endpoint": 3.329.0 + "@aws-sdk/middleware-host-header": 3.329.0 + "@aws-sdk/middleware-logger": 3.329.0 + "@aws-sdk/middleware-recursion-detection": 3.329.0 + "@aws-sdk/middleware-retry": 3.329.0 + "@aws-sdk/middleware-sdk-sts": 3.329.0 + "@aws-sdk/middleware-serde": 3.329.0 + "@aws-sdk/middleware-signing": 3.329.0 + "@aws-sdk/middleware-stack": 3.329.0 + "@aws-sdk/middleware-user-agent": 3.329.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/node-http-handler": 3.329.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/smithy-client": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/url-parser": 3.329.0 "@aws-sdk/util-base64": 3.310.0 "@aws-sdk/util-body-length-browser": 3.310.0 "@aws-sdk/util-body-length-node": 3.310.0 - "@aws-sdk/util-defaults-mode-browser": 3.310.0 - "@aws-sdk/util-defaults-mode-node": 3.310.0 - "@aws-sdk/util-endpoints": 3.310.0 - "@aws-sdk/util-retry": 3.310.0 - "@aws-sdk/util-user-agent-browser": 3.310.0 - "@aws-sdk/util-user-agent-node": 3.310.0 + "@aws-sdk/util-defaults-mode-browser": 3.329.0 + "@aws-sdk/util-defaults-mode-node": 3.329.0 + "@aws-sdk/util-endpoints": 3.329.0 + "@aws-sdk/util-retry": 3.329.0 + "@aws-sdk/util-user-agent-browser": 3.329.0 + "@aws-sdk/util-user-agent-node": 3.329.0 "@aws-sdk/util-utf8": 3.310.0 fast-xml-parser: 4.1.2 tslib: ^2.5.0 - checksum: ad0b444605cbb53caf7ef2c637a79923adeca133d8fc74ee45208828bc8546ae69a2738ce77f6aae376c53d41e2f59ec7703d4a3c364066cacef06427a746e8c + checksum: f9dc6bf6cf640bff23f5aed792921cbf7048df88c840092b4727f8e32c4a14abf420e939a319167ee6bbcf7a1f5f61774d980a6c03e88afe516f7b662361a46a languageName: node linkType: hard -"@aws-sdk/config-resolver@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/config-resolver@npm:3.310.0" +"@aws-sdk/config-resolver@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/config-resolver@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-config-provider": 3.310.0 - "@aws-sdk/util-middleware": 3.310.0 + "@aws-sdk/util-middleware": 3.329.0 tslib: ^2.5.0 - checksum: ec80bc867304344d04b6d6bbf369234e7d296540ffb988c3f29bf96ea5e3ac959f86a699c8eecef3fea87491fd413aaed3137e4f7a89544cceedf09cce1c9a15 + checksum: c5838c0a60e955cda5fcd91361b168fe1bbedd997c9c4482383c98f2690149114f902d18a26ef3188b62734f2f3754cafed6283d4349d83eeb0d072b081f4ead languageName: node linkType: hard -"@aws-sdk/credential-provider-cognito-identity@npm:3.312.0": - version: 3.312.0 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.312.0" +"@aws-sdk/credential-provider-cognito-identity@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.329.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.312.0 - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/client-cognito-identity": 3.329.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 4acd9f7bbd8cb9adabbd9b6465fe81f7bd47d5808b3a5fd3352e4e573a8ff718e56684e67631649f72d01078dc858278de1b5c227b8557d948d4c43abde99561 + checksum: d0d82fbe2cd88862dad13628de417c16c983b8fadd319b3a459d16298a25f90d34d7f054b68a688a292d590b73384d937fac6a1fbb18b77d262c5c5edaf95f94 languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.310.0" +"@aws-sdk/credential-provider-env@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.329.0" dependencies: - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 646e634e6f8429c1984475100a60066dd5d0c085b3e170dc0c05c55c824edb3b04d4c40496ab4318e9586b9ca1db0b20090d26919b0273351c82372a12cd9958 + checksum: 12cf04f542c722296adc4e6fbfe63334a60e7b1763b5e1ebfbaafcdcb332555d1f8cf9c855c249b056fb560a008957df4b7fd1551c04a7dbbebc6e3baad89eaa languageName: node linkType: hard -"@aws-sdk/credential-provider-imds@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/credential-provider-imds@npm:3.310.0" +"@aws-sdk/credential-provider-imds@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/credential-provider-imds@npm:3.329.0" dependencies: - "@aws-sdk/node-config-provider": 3.310.0 - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/url-parser": 3.310.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/url-parser": 3.329.0 tslib: ^2.5.0 - checksum: 24915e2f108e37bef21b9bec07f7ab38f25bf3ed55c55ad318ae8e030e042123980855b977c13714580232d2c0a514e71efd61848e68c221716c2110c160ab13 + checksum: 853b42758e271f85b290aef5d5a2d717b3231016f1541a657408572c4a8fe7f2a209450bc1133715007b895d3c74d065b90a229e31351493bacf368720676471 languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.310.0" +"@aws-sdk/credential-provider-ini@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.329.0" dependencies: - "@aws-sdk/credential-provider-env": 3.310.0 - "@aws-sdk/credential-provider-imds": 3.310.0 - "@aws-sdk/credential-provider-process": 3.310.0 - "@aws-sdk/credential-provider-sso": 3.310.0 - "@aws-sdk/credential-provider-web-identity": 3.310.0 - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/shared-ini-file-loader": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/credential-provider-env": 3.329.0 + "@aws-sdk/credential-provider-imds": 3.329.0 + "@aws-sdk/credential-provider-process": 3.329.0 + "@aws-sdk/credential-provider-sso": 3.329.0 + "@aws-sdk/credential-provider-web-identity": 3.329.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/shared-ini-file-loader": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 9d0adc90a942680ccc1aa135b1d5b42a32a1db1aee0a1d1cfeac2f20347b28a5b8a33bb982eec9e2ff1204ffacfff3cc4b1065cdd11f035edcde4142ff3e3125 + checksum: 2fe427272d08e1baad780d9b7ce53badf2fa4f58f54f65f6b21ba6e54b32bc908dc18a1e934ba39148f7293f78e6b8009e0c22e503e6f3c2d25b0f2c54534ba7 languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:3.310.0, @aws-sdk/credential-provider-node@npm:^3.208.0": - version: 3.310.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.310.0" +"@aws-sdk/credential-provider-node@npm:3.329.0, @aws-sdk/credential-provider-node@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.329.0" dependencies: - "@aws-sdk/credential-provider-env": 3.310.0 - "@aws-sdk/credential-provider-imds": 3.310.0 - "@aws-sdk/credential-provider-ini": 3.310.0 - "@aws-sdk/credential-provider-process": 3.310.0 - "@aws-sdk/credential-provider-sso": 3.310.0 - "@aws-sdk/credential-provider-web-identity": 3.310.0 - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/shared-ini-file-loader": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/credential-provider-env": 3.329.0 + "@aws-sdk/credential-provider-imds": 3.329.0 + "@aws-sdk/credential-provider-ini": 3.329.0 + "@aws-sdk/credential-provider-process": 3.329.0 + "@aws-sdk/credential-provider-sso": 3.329.0 + "@aws-sdk/credential-provider-web-identity": 3.329.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/shared-ini-file-loader": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 893aacd840a69363e11cbfb50c915aa3f515f40d6f5a93018f2fd1d7775af40d2a2fd083422aabc817f433e0f89fe5cdaaea7059b0ec59e8f38c78c97b35995a + checksum: 472bc96b16c589db5e2f938f9f5cc29236ca62186d988b2b06b1bd9378ed681a3e290c9ba4b4adbaabf803e0a1182060df0b5c071ba30a47c8e302f4f55b22a1 languageName: node linkType: hard -"@aws-sdk/credential-provider-process@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.310.0" +"@aws-sdk/credential-provider-process@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.329.0" dependencies: - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/shared-ini-file-loader": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/shared-ini-file-loader": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 12c4ab1f34d5a045d56ca22bc6c834292da15d518129133babcbede056adb46f4e898489e1b54e7e5ee3472d1116882217f5a29af0a46cc40d2f3aa00ef6767f + checksum: 198fb80efab5a29ea0d72e96f0f45adbe3532b56deca045c7cc4089bf7a5388f73e340df135525f4392d942e3a1c516d3cd692d4de7f4d4e6ef9241d7470ee7a languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.310.0" +"@aws-sdk/credential-provider-sso@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.329.0" dependencies: - "@aws-sdk/client-sso": 3.310.0 - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/shared-ini-file-loader": 3.310.0 - "@aws-sdk/token-providers": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/client-sso": 3.329.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/shared-ini-file-loader": 3.329.0 + "@aws-sdk/token-providers": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 595c3f393430e1c9ddb0b276b605e87a8a6d461fe5d7bfa7e9b033fd06f3d5bcb1f4fe6b74e7eb5fa38d1d88181f8be5398bb3c1a53ae18caada9a59eed1ba21 + checksum: 0c925dc2c5a76606e0338240a1348bce52bee94c6d3f2d7dec179168439857abe0b4c9ff5ab29d59af5af4c7e2228f2ac1dad0f86803f68f1574dbcd9afa96f3 languageName: node linkType: hard -"@aws-sdk/credential-provider-web-identity@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.310.0" +"@aws-sdk/credential-provider-web-identity@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.329.0" dependencies: - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 62dd9362bb48e010cb84dfcb92461478b2d1fa830e47e078a9bd074999eb231b0ef4e273e585fce5ed0135768b90bafcbe9ca5df83fc6c0bc5d227ec74271a82 + checksum: fcbc21976d471999285904cb4924b49d85d846ecc3495a2fc6a5e8ff611765da1fdfd98f5cc828af29e43a0e087c3f49dae0186ef8e43de4afd07bec43c6fc69 languageName: node linkType: hard -"@aws-sdk/credential-providers@npm:^3.208.0": - version: 3.312.0 - resolution: "@aws-sdk/credential-providers@npm:3.312.0" +"@aws-sdk/credential-providers@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/credential-providers@npm:3.329.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.312.0 - "@aws-sdk/client-sso": 3.310.0 - "@aws-sdk/client-sts": 3.312.0 - "@aws-sdk/credential-provider-cognito-identity": 3.312.0 - "@aws-sdk/credential-provider-env": 3.310.0 - "@aws-sdk/credential-provider-imds": 3.310.0 - "@aws-sdk/credential-provider-ini": 3.310.0 - "@aws-sdk/credential-provider-node": 3.310.0 - "@aws-sdk/credential-provider-process": 3.310.0 - "@aws-sdk/credential-provider-sso": 3.310.0 - "@aws-sdk/credential-provider-web-identity": 3.310.0 - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/client-cognito-identity": 3.329.0 + "@aws-sdk/client-sso": 3.329.0 + "@aws-sdk/client-sts": 3.329.0 + "@aws-sdk/credential-provider-cognito-identity": 3.329.0 + "@aws-sdk/credential-provider-env": 3.329.0 + "@aws-sdk/credential-provider-imds": 3.329.0 + "@aws-sdk/credential-provider-ini": 3.329.0 + "@aws-sdk/credential-provider-node": 3.329.0 + "@aws-sdk/credential-provider-process": 3.329.0 + "@aws-sdk/credential-provider-sso": 3.329.0 + "@aws-sdk/credential-provider-web-identity": 3.329.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: d47501b81e3f37c50110a12f58491407c95bebfa341ed80040aeefab236a3496dc33100da244e1e4467d2e2d62be19faec04f2e52a236a5c5ec2e1feeb12ceee + checksum: 534ac502c710d23750648166b9a1d51099cc64f23e9fbda586ddbb2cfb6a393b99cd18a0ad5f5f6c431ca106f4e752b2e6bd71ba309b1fb3ca45145c04a90ef7 languageName: node linkType: hard -"@aws-sdk/eventstream-codec@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/eventstream-codec@npm:3.310.0" +"@aws-sdk/eventstream-codec@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/eventstream-codec@npm:3.329.0" dependencies: "@aws-crypto/crc32": 3.0.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-hex-encoding": 3.310.0 tslib: ^2.5.0 - checksum: 2866186a5303bb8b9151ea69f22bace420317c4ec3b5c9aa0beb9bd007aca419a519be2d911b8119ddfd1c0a8358cc9e0f957ca3a460b3009846e0e5517a8f7a + checksum: c57070e439b8a9009cb11f47b51f233b2fc6cff785a6e8ab869e8d7b299dd358fd14053f943ef14fbf1d4affd7e23de8f976ece09d4b180ca0a053e74724d35b languageName: node linkType: hard -"@aws-sdk/eventstream-serde-browser@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/eventstream-serde-browser@npm:3.310.0" +"@aws-sdk/eventstream-serde-browser@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/eventstream-serde-browser@npm:3.329.0" dependencies: - "@aws-sdk/eventstream-serde-universal": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/eventstream-serde-universal": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 10a8dc5999c3f8f683ef399c8866d274232e235bf526c481da2527c24e23836e362c647c92103dcabdf52107b8fd0c46ae8e9f8d2f79741f198022cd94b150f1 + checksum: 07d318f9bf41427947dfe885f1fe4b1f2a08934fd43db4c44fd941baf92bb989e8b9e380597e0b7834e3190e697dd72ab2ba5c8fff3437ab84efae3706a5e217 languageName: node linkType: hard -"@aws-sdk/eventstream-serde-config-resolver@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/eventstream-serde-config-resolver@npm:3.310.0" +"@aws-sdk/eventstream-serde-config-resolver@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/eventstream-serde-config-resolver@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 9ad39115d8bd163fe441fa8128e102c1e6157890c7731d7ab797b63a636f378f30a2695adcf41b58f52b6bfd71b68c2bafe51d1cf4f984b124746341d13ced6b + checksum: 51a4c94beffcf74f956f1750a99788fdfd15d1dcddbc5ab2b5c8d0a089a430cad16b2b27515aac84c4dc0ff304a8d69212a2af411e921eb3fd59f49ed3ef7b48 languageName: node linkType: hard -"@aws-sdk/eventstream-serde-node@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/eventstream-serde-node@npm:3.310.0" +"@aws-sdk/eventstream-serde-node@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/eventstream-serde-node@npm:3.329.0" dependencies: - "@aws-sdk/eventstream-serde-universal": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/eventstream-serde-universal": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 1a695e39e5bbd4088d5790c8b4389dd635b7f6300892868d35e8711ef7ea5c1f2f50886c9ac55de5eae9619c49fe864c6e530e575a06231300294211a36827b3 + checksum: 398fd78b194eef3a95f84d056d5d1690e0342a3f643aae2ac40465cefa81ff21dcd299d93d53ffbdf22c49807a932c6798d17a913f466f3f7ed4c9b23ea136fa languageName: node linkType: hard -"@aws-sdk/eventstream-serde-universal@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/eventstream-serde-universal@npm:3.310.0" +"@aws-sdk/eventstream-serde-universal@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/eventstream-serde-universal@npm:3.329.0" dependencies: - "@aws-sdk/eventstream-codec": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/eventstream-codec": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 10ce20184cdfd826600f610418e07add4e70073470cf23a5b3b8b4ea479ab096eb70c6eeb9e87bcd30e040270b3c0519fe187aad3363c85bc357a048014a8de4 + checksum: ffdaa375d9a97079380514849b4f561ba24955da754f5bfebc9406dc43fe93674c0275278d27b2568313056a04be3de6f1783f7a471b501e84736e1f2c95155a languageName: node linkType: hard -"@aws-sdk/fetch-http-handler@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/fetch-http-handler@npm:3.310.0" +"@aws-sdk/fetch-http-handler@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/fetch-http-handler@npm:3.329.0" dependencies: - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/querystring-builder": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/querystring-builder": 3.329.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-base64": 3.310.0 tslib: ^2.5.0 - checksum: 5daa78ee3e2a0a6bd07c3b8bc658ebd88a063b17025ec23454c2eb433859972d60a550fdc62969754488c3f4d624fbf3e758af8ea891c994998deca0f8e3903e + checksum: 85795cd97f1538ed4d713b370377667f18bf3ead1326815452b8636e6ae07af3726f75bb95f2ad31d6fa06eb802903344f3413724ca4e593fbe8a05c533c8e52 languageName: node linkType: hard -"@aws-sdk/hash-blob-browser@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/hash-blob-browser@npm:3.310.0" +"@aws-sdk/hash-blob-browser@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/hash-blob-browser@npm:3.329.0" dependencies: "@aws-sdk/chunked-blob-reader": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 58908f51a89f61b5c0ed036beeda58d7e54d1385ca56447c057569671b110be0dad2bec4618daa35a8170c8b3e0e193bd473f695d0be269ec2dd06915bcaa9b9 + checksum: 9b8e0a5ae442f0cce372bf79a0e634c6b99eb4fa39cea91bda437de51e4da4fbe288e0229c04f778d865b9281e2e93fbfe0b269a492ecd7285e9f84f3816cb24 languageName: node linkType: hard -"@aws-sdk/hash-node@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/hash-node@npm:3.310.0" +"@aws-sdk/hash-node@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/hash-node@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-buffer-from": 3.310.0 "@aws-sdk/util-utf8": 3.310.0 tslib: ^2.5.0 - checksum: 379c04c78679d68730272b89fa397cdfcd444ae2f21d7dc51953e9885842469de40593efbb86b2399342e022b2ba17926841ef0a9fb108809296b2df416226c1 + checksum: 19f505bc9ef34b15277acb13cfed9f06efc6a4d648df45aed5eb12c0c3566a6338e00bf126081fb5429149c69ba99b9120aebe5940eb91ac7a6686a5f0c9cabf languageName: node linkType: hard -"@aws-sdk/hash-stream-node@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/hash-stream-node@npm:3.310.0" +"@aws-sdk/hash-stream-node@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/hash-stream-node@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-utf8": 3.310.0 tslib: ^2.5.0 - checksum: 42fe6f8eddd67565019d935b109e9f3acf0eacfd940712859e718db8f1ca4550c305f0fc4dad9944624ea10e0ff7e15f895d13cce0cdcf5c614d3bff39263ecf + checksum: 3901b6fa62e9e09a21c16c13f929a401338366c12a0ba695b830a5195859740cc6853cb22cc01c164fbfb9db817dabac2f260033cbb027f506abaae7a499f2c5 languageName: node linkType: hard -"@aws-sdk/invalid-dependency@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/invalid-dependency@npm:3.310.0" +"@aws-sdk/invalid-dependency@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/invalid-dependency@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: e38d09615e273617583e845b2cd3683c9d27d54234a98bec7da1cf959107329f73e62b322479415155016ed62c7d849cd6542d6e9e33572f6ed542013c15821c + checksum: 0e5f122ab2529386f7531bdf1494968264b6e2addd89e484bb3f28ad3bd8d438d381923e61cfd4ea3dfe930982d484541d9b8c562a19de90fec06791d6bcc586 languageName: node linkType: hard @@ -1088,12 +1176,12 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/lib-storage@npm:^3.208.0": - version: 3.312.0 - resolution: "@aws-sdk/lib-storage@npm:3.312.0" +"@aws-sdk/lib-storage@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/lib-storage@npm:3.329.0" dependencies: - "@aws-sdk/middleware-endpoint": 3.310.0 - "@aws-sdk/smithy-client": 3.310.0 + "@aws-sdk/middleware-endpoint": 3.329.0 + "@aws-sdk/smithy-client": 3.329.0 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 @@ -1101,391 +1189,391 @@ __metadata: peerDependencies: "@aws-sdk/abort-controller": ^3.0.0 "@aws-sdk/client-s3": ^3.0.0 - checksum: ce09963885a749e6347ffe8d2ea94e2572629f31041f7859f71d98436ff863822862c13c2c465af5a11aacc3193d0c599f19d455b8251ebcbe95fc44eba1cd0e + checksum: 47e3ec6cc4882e6b016c3a161ca0bbfd733f3b6a89a3ee2af80df17ca14113104df79f32d901ce460899da8d01182fc7fddc1ddef3858c5e2020ffa29231ed20 languageName: node linkType: hard -"@aws-sdk/md5-js@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/md5-js@npm:3.310.0" +"@aws-sdk/md5-js@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/md5-js@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-utf8": 3.310.0 tslib: ^2.5.0 - checksum: 1939591f5f384e55ce9d610cbf3e8ae25e76c14489385ef73ee0c674b641351820d17e2406943e2b3ee4a3736d280256f2b79bb381e18e78440faf74d4ca04cf + checksum: 3471b9c5e62a4c37c609a6575ea521028cd2852bbaf1a55e8d077ed0c4d90c4339e6c6a3015553e9549d1e519ade4d68acc98571a1fa9e880d66c907cfd12977 languageName: node linkType: hard -"@aws-sdk/middleware-bucket-endpoint@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.310.0" +"@aws-sdk/middleware-bucket-endpoint@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.329.0" dependencies: - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-arn-parser": 3.310.0 "@aws-sdk/util-config-provider": 3.310.0 tslib: ^2.5.0 - checksum: cf300c0226a649bdd7128c5c44ff03bac58119390c8bf82a712094e95c2df4c0b1693a74256968294d8ad397828fb2f7d5f0d9355c08d1d67a24888255d6dfd2 + checksum: bda4e039ee9655d528e6416005bb4efa13e9dcd16a7c16d1693ed7970702bce3c2ff8ea6204a3c090c9881cf7a6ccf2385f44ac9fcc9b8d8addf6a27fa93d56b languageName: node linkType: hard -"@aws-sdk/middleware-content-length@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-content-length@npm:3.310.0" +"@aws-sdk/middleware-content-length@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-content-length@npm:3.329.0" dependencies: - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 66977eac6aa9ce0d7c5640e357608b79eec18919d4e94c37aacf76801ac1b24471a3483755d1ab30a416aa2aa10c9da02fb6241f11e29ea99079ff04bdf012b1 + checksum: ffc2f7d2c7b2c7398d54b118c7392816f8e24ac4b350794f0c501a9ca82c67d4b6f90d9442f6ffa71b3e1f75da20b5274390b0a18d8f4adcedf5893251023588 languageName: node linkType: hard -"@aws-sdk/middleware-endpoint@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-endpoint@npm:3.310.0" +"@aws-sdk/middleware-endpoint@npm:3.329.0, @aws-sdk/middleware-endpoint@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-endpoint@npm:3.329.0" dependencies: - "@aws-sdk/middleware-serde": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/url-parser": 3.310.0 - "@aws-sdk/util-middleware": 3.310.0 + "@aws-sdk/middleware-serde": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/url-parser": 3.329.0 + "@aws-sdk/util-middleware": 3.329.0 tslib: ^2.5.0 - checksum: 7d61ceaff3fb6be779f9b0597fceccbc1d5ebbc83b83d93ac184fc6451e60b4acca9eebb0c83c9e1c6b34400bd39345b498227860892ed51eda2b99f16ff0566 + checksum: ed662d778a4ab7b05ee4fba607d6b72e798ee969752fabe2141546a184a71f2daf206a2b8300fa68de0339ebc2646fb8fdf9bf22ebf9cf68495518465c1f1bcc languageName: node linkType: hard -"@aws-sdk/middleware-expect-continue@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.310.0" +"@aws-sdk/middleware-expect-continue@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.329.0" dependencies: - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 717eb502c00bc5b32cbb210657b0cfdec837f5296f86043bce40f43535f345e689639b017c71271709bdee0a58cc4ade6324ae0bcccfcdc0789e8699b2ce9f76 + checksum: 462dd314e80ce17e5975e4b4ce162d41a44691845dfff6b6ae7bd0eb449402bd2d1f93ab577513faf089c9fb193b9a79ccd1ccfb1eb917576e42c2618e4d2ef9 languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.310.0" +"@aws-sdk/middleware-flexible-checksums@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.329.0" dependencies: "@aws-crypto/crc32": 3.0.0 "@aws-crypto/crc32c": 3.0.0 "@aws-sdk/is-array-buffer": 3.310.0 - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-utf8": 3.310.0 tslib: ^2.5.0 - checksum: be99290968c5e00053dd2e43683d77cd08dbe1acff64189e522d9e4aa1440c8e6ae86d784ef4d163a059f725b52fdd5812b54594d70a9773c25feef24e63c9c3 + checksum: 06a3ca6c7dfd679c092d2f504caf8c72107243efa408f9e04055e4ce74dd32806b5f786f84dd6e9586c4ba92c81f7f3817e7191df45546c775b8f9b682e266f5 languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.310.0" +"@aws-sdk/middleware-host-header@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.329.0" dependencies: - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 9b1bf8598f9bf44a0cd992f08820ce54fb7ce5f33366796b7328a003c2efc00754a3e0bfd56be87b221ca0f15b4c00f5caf736bf196cb9a4b3ca26dfd3e7f7db + checksum: a83d44c24fedf605cfc1b715f5529fc5c424c11ef77a29fa483e390d379a0ca0a4839249d64e71a8a3f096a2ed97ddf95fc8726149bbb9343f50d6522f0ed46b languageName: node linkType: hard -"@aws-sdk/middleware-location-constraint@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.310.0" +"@aws-sdk/middleware-location-constraint@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 2ca1a7d9ca47c29468c4286731f0a1dc15e0233f0a59bc1dad3f3ed5917b1e5d7b344c705660cc3afdecb153f2e2f203e9ae37eb018d6a56aaeba7ad05ebd466 + checksum: 04565ae85637cec8c2d0fc574479e36cc953c86e9ec22e4ebc22d52b832b2d4e50b71750f72d4ddcabf1ce820ce8edccf9efb5fadcf292e9ad842289d16999c9 languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-logger@npm:3.310.0" +"@aws-sdk/middleware-logger@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-logger@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 13014451afaadf11524754f959aaa4c4e7763442dedef841d693159370720e40d20a6113851b87b6cab6c709d92b1e952adede0ec9948dbaa1546dbff1e477d0 + checksum: 85031d5d562ae34a550c9ce355cce2e322924dfbb6140e255444056a6f97ab3a98db9d9854572c0d1cf2d73e0e086320a6324cabedac66e975007f1140148c49 languageName: node linkType: hard -"@aws-sdk/middleware-recursion-detection@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.310.0" +"@aws-sdk/middleware-recursion-detection@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.329.0" dependencies: - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: a5db6bec59a5232ebc28296d165d09fb94d74e9232d32f49f77bccbbae62cda58215d2f8a17979f1714b9dd07c25a989caae8bc7eee1f57c57d67328788fa401 + checksum: 921683327a3047d9fa8c1c027bee44330ae769a45bdd205a7a5d1a262fde09c404c574a60497e87987f01670daf56b2e0084fdacded87270917f04e99ac437a4 languageName: node linkType: hard -"@aws-sdk/middleware-retry@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-retry@npm:3.310.0" +"@aws-sdk/middleware-retry@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-retry@npm:3.329.0" dependencies: - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/service-error-classification": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/util-middleware": 3.310.0 - "@aws-sdk/util-retry": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/service-error-classification": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/util-middleware": 3.329.0 + "@aws-sdk/util-retry": 3.329.0 tslib: ^2.5.0 uuid: ^8.3.2 - checksum: 7d69c187d4cfad62df01b445596f812157e4028b377f34c40f6b272df3660a48ebbc6a0c86eba98b1b19454ade6be7b1459c62ffe8a1924725a23e330d2814b7 + checksum: 1c3eed0037a06c5201a8c102c1d5b946571a8085bde0ccc9fafb3c62343414383ee94883c07e7b2e4e870852a7a54798e1cbaeda17062d7a4b7178f6355f3119 languageName: node linkType: hard -"@aws-sdk/middleware-sdk-s3@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.310.0" +"@aws-sdk/middleware-sdk-s3@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.329.0" dependencies: - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-arn-parser": 3.310.0 tslib: ^2.5.0 - checksum: b546560c2fc3fcbb37d38b5d02e3f8349592d66a6a2ef4571b72d0bb6c398d0c2bb7008665d58541efc577372f2e1aae88ea1318ba0fe8613583397604dff02e + checksum: 4de1fc7b4ac6b01c3e65ee8e25151b73d3f78d5ac3949d799b10252a42a8b55bb00242822757bbc790dde545745fb8d2f83feb09442eec83846c431d98aeab27 languageName: node linkType: hard -"@aws-sdk/middleware-sdk-sqs@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.310.0" +"@aws-sdk/middleware-sdk-sqs@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-hex-encoding": 3.310.0 "@aws-sdk/util-utf8": 3.310.0 tslib: ^2.5.0 - checksum: b8207b1625e52d02b3a3f28153e84f3a9c44bc9812ceb0ca1da798aa9fe576b9aef26c2da10dd6c8742c64e734eec93fd5123d769079b99a19bd105599fe11e5 + checksum: 77faadbf799b37ac036cba465174cabbc5557aa59944c20d05b8253e54266aed7846bf6135f7b7f0ab183da555ba255782e257a3d4364a3ffe0efe6ea738f97a languageName: node linkType: hard -"@aws-sdk/middleware-sdk-sts@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-sdk-sts@npm:3.310.0" +"@aws-sdk/middleware-sdk-sts@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-sdk-sts@npm:3.329.0" dependencies: - "@aws-sdk/middleware-signing": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/middleware-signing": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 80debd2f2371f65f7c37f2f0101e9e8ac520ef74d6a8ba54fedfbad6d63653732f7ce6095bae7bf3adbfec61bfa4d9f816b8eb5550cdadec825b400cf74bb2ce + checksum: 0c5450726c546f6f4b3de50e1a9a1757ca267f6fece5ef3dcf1560955eea885d0476debbce0d3f67290db77e7b894a386f5822b7d5bde8d9bcfea5af94735918 languageName: node linkType: hard -"@aws-sdk/middleware-serde@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-serde@npm:3.310.0" +"@aws-sdk/middleware-serde@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-serde@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 95c2c1b15906a93c9869be36563757f08cd53a0f385882759943e59a1fd31be777260fb075feaa1a9bb919cf1696739e7b2da89049cec0bee1a649a838f9184c + checksum: 29b72c582f6c783396e6e7a5a9bd6416ceebb18a15b55187267cdb3e2a6f18e2395d2aafdbeedeedce8c014b0d7fbd40d151accc1e409fe8741cee346b0c9ce1 languageName: node linkType: hard -"@aws-sdk/middleware-signing@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-signing@npm:3.310.0" +"@aws-sdk/middleware-signing@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-signing@npm:3.329.0" dependencies: - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/signature-v4": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/util-middleware": 3.310.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/signature-v4": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/util-middleware": 3.329.0 tslib: ^2.5.0 - checksum: f1db11435250075fc563de375c8c513dbaba7b9939ae99c70074d90622f9aea0cc339cd10f0eff63251eba462b73f564389bfb9dcfe6868f36892488dea0494b + checksum: 6dad12d4e3cfad3dd508ad2177da3ec5d54015d606c670999659c21dfaaf41b1cbb52c92704a9ecf0f82e322ae9f62a0639c2e3cf1b201c08d78b99638abdd1c languageName: node linkType: hard -"@aws-sdk/middleware-ssec@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-ssec@npm:3.310.0" +"@aws-sdk/middleware-ssec@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-ssec@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: a6d9cde8d826c4baae7908e82035e892f8b4706e648efa1ff5a8706285efabd17f9fe8d48b5b0fb42b17f227fba257b7d95b99baf9e152246cb95a2fe23a547f + checksum: 819623a8a3a8d3518fcc194025a470a06d0b6889cb88f165a7f49aea6b1e4396effe448ac94b8a27928aca8752502960d6899a5597f293f0542ee9d0e5a872da languageName: node linkType: hard -"@aws-sdk/middleware-stack@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-stack@npm:3.310.0" +"@aws-sdk/middleware-stack@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-stack@npm:3.329.0" dependencies: tslib: ^2.5.0 - checksum: ad90bb8cf2a8e3211869ed0c08e240e0df7097ff42a9bbfa6dd96ad79a8b741c096199082f1be40a2ae2b1fbeb56a4bc510cdaf431dd90a5db73e32fe7184ee2 + checksum: 34b2ac02f8bd21a6803064de8059a866aa776c8a7962d40e11d072801291b9f5e78daac5adf91931ca1d19b11e2e972467c6b9679bf7c049b5f63ce50eb214d9 languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.310.0" +"@aws-sdk/middleware-user-agent@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.329.0" dependencies: - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/types": 3.310.0 - "@aws-sdk/util-endpoints": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/types": 3.329.0 + "@aws-sdk/util-endpoints": 3.329.0 tslib: ^2.5.0 - checksum: 571ebfce481a2d814270248e728c68030661cb3f0de97908b73e1d446c508d5af06cb088c68c67b23ce9671873c1872ab681b9383d48797b5a1c53a7030085ff + checksum: 6241246ea640bbe23148fa632e6735f6f25c3e1d32d01bcc21e733b2c11aba4c899acfe9982d98eab4d0dcddc451d60c8d6710ed1240e61ad428df24d10cc64e languageName: node linkType: hard -"@aws-sdk/node-config-provider@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/node-config-provider@npm:3.310.0" +"@aws-sdk/node-config-provider@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/node-config-provider@npm:3.329.0" dependencies: - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/shared-ini-file-loader": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/shared-ini-file-loader": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 95d017aa1bb94e323c288bc0ce5edba5c4605eeabe779249beb5faee958c26f6eebb7f1664328b83d1024e441eb4e4f9fce9c1bb764637f83f7ebf20b8359a77 + checksum: 25178c8857f48931079b204e2c904f7960edbae2a34bb6ccb8f27991d807a61e05dcf42cad4bb56cb98c5c12aedebea58a840b3bea30a06c174cb6758b03e2d5 languageName: node linkType: hard -"@aws-sdk/node-http-handler@npm:3.310.0, @aws-sdk/node-http-handler@npm:^3.208.0": - version: 3.310.0 - resolution: "@aws-sdk/node-http-handler@npm:3.310.0" +"@aws-sdk/node-http-handler@npm:3.329.0, @aws-sdk/node-http-handler@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/node-http-handler@npm:3.329.0" dependencies: - "@aws-sdk/abort-controller": 3.310.0 - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/querystring-builder": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/abort-controller": 3.329.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/querystring-builder": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 781cc864972bf52f884b580e43b9b659ab34a6ca7d7772d8e76107a51fe0930124c01024bc7ac1c4e99324319c594b809373ebc4752ea0a2e3a984ccf57aa535 + checksum: 0560640be504cbe1f4d5098cdef917085472f476487d2fcd5225ab67063dca53718a30d48cb255790d327356f8b544bba491175dd2d6c9b8319327f5204909ae languageName: node linkType: hard -"@aws-sdk/property-provider@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/property-provider@npm:3.310.0" +"@aws-sdk/property-provider@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/property-provider@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 8a906b3f4e482f4d5be0ef1277fcb22fb005e834c916919373187f8cf6b17b0d464f37a12770d152a553b7a505ed9981504a0c30f73f273d251ed93ff29616e1 + checksum: 17c8cb7d28b55ff9ba1f81da87dff42815919c6458eec422d1a45d7d809f7ee531e7b5daeb2bb63eab569eb12c0d6bce270a66e060c8c19d841d1894211f6dfd languageName: node linkType: hard -"@aws-sdk/protocol-http@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/protocol-http@npm:3.310.0" +"@aws-sdk/protocol-http@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/protocol-http@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 4bfe2b7a93d52ded21472d6347483fb52dfd2414d4ff07d8e3a2869d7676e866a9bfa29e9e7ac4fa3849c7109740a39e3d1e646a02d8bb4b7c7b402f53b18450 + checksum: 6b88be86283c67fd6fc9c3f8260c02e916f6f9b4c2a554a261809d6e48ef3dbae0a6c02ecde90cbfcd77f8066dbb37c2263e22fbeb20c2a5ac19e3ffc2582b67 languageName: node linkType: hard -"@aws-sdk/querystring-builder@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/querystring-builder@npm:3.310.0" +"@aws-sdk/querystring-builder@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/querystring-builder@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-uri-escape": 3.310.0 tslib: ^2.5.0 - checksum: c06ba9ec67d6e6a5f4c1099461b9b2d6cb12a278e6ec2fe198f68ba115ce1e05425f29cf6859f8a005ae7123036b6dadc325d18b35165c7049233f9d04670dcb + checksum: 188a3e6ee142af8ec8fa6563db831e70069a4f94487f25b02da608fe569d881c4b696abff8bbf98ff154107ed4b57170f1b6633ec55a7ddbffabb3cd745c243f languageName: node linkType: hard -"@aws-sdk/querystring-parser@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/querystring-parser@npm:3.310.0" +"@aws-sdk/querystring-parser@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/querystring-parser@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 5e9d8700918db3daa89440f7c0aa9d0ee37e30bab13892f12602267259160ff73eaccd7e01521bf71f4a0f59da9cb632f75e583d927900f2acddc4913e3422f8 + checksum: eebb633db0bf9d9a3854ab2f83526468b430f57fb8b5cd2c737683facf717d96876acb4a837e17235458e728ebf375a26321ea6dd4dc07f5eb9eaa6e4efad1b5 languageName: node linkType: hard -"@aws-sdk/service-error-classification@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/service-error-classification@npm:3.310.0" - checksum: a600a7634fe932b52676ea33851230173ce66b45f4c8350c91616e37f9cbd43e8f6e7e3fc9761fd14ca7ecd2c7ca90ca806fc555e383d0bf0ee2bdb6a4d73888 +"@aws-sdk/service-error-classification@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/service-error-classification@npm:3.329.0" + checksum: 77b19ffd3fa926b3e4b5f5aee64ff3159af6fa7c9f3a643fe7505690a2bd7d67e2ff8790ea3be21458c95c0eeb65687e4dd01140c352d170549368c0ff908936 languageName: node linkType: hard -"@aws-sdk/shared-ini-file-loader@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/shared-ini-file-loader@npm:3.310.0" +"@aws-sdk/shared-ini-file-loader@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/shared-ini-file-loader@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: aa3ffb5cb4320ee936102be200dbacb95be0bd85088c692de268d56c175dd4329757a83847d1c4e689b98f3810f729596a1a0b726f1ea0a8d00c78516fc10cc3 + checksum: ee5e57d38a17eb4ebb073daa3e1f91bf07c3a5746410470a628765e0b49176ac880a86b6e09279137fdda7a44a38666cb311fd3ed523d9a3c4d4766d1ebeeb7e languageName: node linkType: hard -"@aws-sdk/signature-v4-multi-region@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.310.0" +"@aws-sdk/signature-v4-multi-region@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.329.0" dependencies: - "@aws-sdk/protocol-http": 3.310.0 - "@aws-sdk/signature-v4": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/protocol-http": 3.329.0 + "@aws-sdk/signature-v4": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 peerDependencies: "@aws-sdk/signature-v4-crt": ^3.118.0 peerDependenciesMeta: "@aws-sdk/signature-v4-crt": optional: true - checksum: ef88c3ba8dba0fda1204ec9358e46ea2bbefb0a258b21e2c83142b3ea9bb9be3eba428b38297da28bb4a5e93f9c27de8711aa6f219b3cbced4eba0c443c9b68a + checksum: c9ae1a68919674b6444cdb64e865e08b72f6a6127c7f43bffbbfb7457d48efc9ea5c89e58e5e451d1ec28c0d735969e4985fee25a7bca5b9949264d8de2763fc languageName: node linkType: hard -"@aws-sdk/signature-v4@npm:3.310.0, @aws-sdk/signature-v4@npm:^3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/signature-v4@npm:3.310.0" +"@aws-sdk/signature-v4@npm:3.329.0, @aws-sdk/signature-v4@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/signature-v4@npm:3.329.0" dependencies: "@aws-sdk/is-array-buffer": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-hex-encoding": 3.310.0 - "@aws-sdk/util-middleware": 3.310.0 + "@aws-sdk/util-middleware": 3.329.0 "@aws-sdk/util-uri-escape": 3.310.0 "@aws-sdk/util-utf8": 3.310.0 tslib: ^2.5.0 - checksum: 0adaf05a005a8a468301f24482d25de3a35554debc98ab8eeb0444c529c02a63dc7e7754d990e9464e1a17c1eb1f6ffdcc178bcd7d35c87587e4cc41574c69b3 + checksum: 8bf81f260f8a6a75e43f172bc049b777d081ca75e8456b21331895c0dba2486b804a8e7aed12ebb178e421aacc515fc873141682d7100c045da60b9425b9c432 languageName: node linkType: hard -"@aws-sdk/smithy-client@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/smithy-client@npm:3.310.0" +"@aws-sdk/smithy-client@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/smithy-client@npm:3.329.0" dependencies: - "@aws-sdk/middleware-stack": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/middleware-stack": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: a97b88e6ae3066abf136fe3d59ead258d459c4f37f69e6170a902d85194cb2d798519771b945880072776bbf0e77bfc18e15768dcbafc402f0f255d80bca0ba6 + checksum: 4235822be0165c44f780a704da01581c8f8476c6a11e2b421d6fa003a30e9ab3bb37c02289b5678194f6d89304b1a576a1fc79b646ac0c58b482f70ea24a4b16 languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/token-providers@npm:3.310.0" +"@aws-sdk/token-providers@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/token-providers@npm:3.329.0" dependencies: - "@aws-sdk/client-sso-oidc": 3.310.0 - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/shared-ini-file-loader": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/client-sso-oidc": 3.329.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/shared-ini-file-loader": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: a38d5c562f0cc29ddcd6b52b8b4c872447824962055cdb77587fdc9e13e24e6a9acda1d7ea60e7b7e2dbfe8b27dc0ff03347e739b298099b7c1ebe67044ff8e1 + checksum: 87d1e4a13ce0cc4af50d4e1d5006967da69d5d1f384edaeea0057ca0a8f3397706934fea152d5f83d7be3c68c9a12c04edb2dbb0558b42cf200066f7d56643a8 languageName: node linkType: hard -"@aws-sdk/types@npm:3.310.0, @aws-sdk/types@npm:^3.208.0, @aws-sdk/types@npm:^3.222.0": - version: 3.310.0 - resolution: "@aws-sdk/types@npm:3.310.0" +"@aws-sdk/types@npm:3.329.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/types@npm:3.329.0" dependencies: tslib: ^2.5.0 - checksum: b11a91899614e14d40081ceab39cd3702254a5658c7b5e8862ef0d66dbffaa41c9a0f0d31e415d22f31c791b507699ba3a5fc7d87a540273386eb779be3807e4 + checksum: 2bbcd8e6ba2f813dc220c60e7ac0fa0d0990d49f88030fd8235479586716016c8f9d7aeaf1f4c1a64694275f33690bd1dd4ed46e127983201061e063da73f426 languageName: node linkType: hard -"@aws-sdk/url-parser@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/url-parser@npm:3.310.0" +"@aws-sdk/url-parser@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/url-parser@npm:3.329.0" dependencies: - "@aws-sdk/querystring-parser": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/querystring-parser": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: a9f5bec1cfa38cf2d244df1f6d7aad0f8e880a285d148678652ba14a3fb03fc0847defdc80a7e3ffb197d91e33d8cfb43325ee39f53c43c40ceb7fbd34f38fda + checksum: d5640fb2c21ebca56904641204bd51bb14b9d7cc1ae76860ce2e7d51cf5c1e76b09eefb98678d67651de2773a7137509017f22a93c8f2e7451666e10100072ec languageName: node linkType: hard -"@aws-sdk/util-arn-parser@npm:3.310.0, @aws-sdk/util-arn-parser@npm:^3.208.0": +"@aws-sdk/util-arn-parser@npm:3.310.0, @aws-sdk/util-arn-parser@npm:^3.310.0": version: 3.310.0 resolution: "@aws-sdk/util-arn-parser@npm:3.310.0" dependencies: @@ -1541,39 +1629,39 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-defaults-mode-browser@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/util-defaults-mode-browser@npm:3.310.0" +"@aws-sdk/util-defaults-mode-browser@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/util-defaults-mode-browser@npm:3.329.0" dependencies: - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/types": 3.329.0 bowser: ^2.11.0 tslib: ^2.5.0 - checksum: 1f1847c41c43e167dec2e6f1a128ac9a486cc2e536b65d8d0e247131105cd2a46329297adc865ec164019850cbd425648add521dbde03417cacceaaf249555c2 + checksum: 217995ed68d7af117a0efa163ec22b0ee1700d4d1a3f4a1a950298e239788743886453dfb91f07c2b6c341497d489cae142872e629f6701c65d689132527be0d languageName: node linkType: hard -"@aws-sdk/util-defaults-mode-node@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/util-defaults-mode-node@npm:3.310.0" +"@aws-sdk/util-defaults-mode-node@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/util-defaults-mode-node@npm:3.329.0" dependencies: - "@aws-sdk/config-resolver": 3.310.0 - "@aws-sdk/credential-provider-imds": 3.310.0 - "@aws-sdk/node-config-provider": 3.310.0 - "@aws-sdk/property-provider": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/config-resolver": 3.329.0 + "@aws-sdk/credential-provider-imds": 3.329.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/property-provider": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: dca18909df6f8c72ae606c25920e2f686016de969aef2be4568cd6ce1fa0c45b3f946702d607b3279d267aaedff8b67251d4ee7bb4f76a38fca4299c81a7757d + checksum: 8b727977205638940da63fe3a4a88862d7a9491c3409543e67a7a9ee071f4c2459d24ca2e078a412710a5fa2146f6644356fbda69b9f3a9046c7d65f14902b8d languageName: node linkType: hard -"@aws-sdk/util-endpoints@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/util-endpoints@npm:3.310.0" +"@aws-sdk/util-endpoints@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/util-endpoints@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 3f1c76b4c7662eeaf9dcf782739aa90812ed6920dd602a4a3779c80fbf3215efb15bd1ad82a30d022b577acd6049d35fbf79b45f6ae842e895be94db360d1b03 + checksum: 6d7c8bcd6a8a14d8597a77799d25fb36a79c0d4453f11257f1964e3230b26d46145a7c98d037f9b238100205733f6e15beb48887a85a52715aa120dbcf02cc69 languageName: node linkType: hard @@ -1595,48 +1683,48 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-middleware@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/util-middleware@npm:3.310.0" +"@aws-sdk/util-middleware@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/util-middleware@npm:3.329.0" dependencies: tslib: ^2.5.0 - checksum: 3c25a83361ce95dd3f66170d67fb39911a3f5bc21627ffaccef1880ad8c3602b6351f5c51e9c0bfef5b4037e5c66b9eadb291a9441db644811cf5640c35c587b + checksum: 23bc1ab195d31871edb2a7c9b3772911e881a39d70dfacedcf6e0308cbe95ec0ccb09f5f41f52458bff621d8e44dd3a4c94336da213d6d8074ffa0c0e334fad2 languageName: node linkType: hard -"@aws-sdk/util-retry@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/util-retry@npm:3.310.0" +"@aws-sdk/util-retry@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/util-retry@npm:3.329.0" dependencies: - "@aws-sdk/service-error-classification": 3.310.0 + "@aws-sdk/service-error-classification": 3.329.0 tslib: ^2.5.0 - checksum: a91b53ca40dd7ac423b46a4916a84567de163e84e63919e77d9a0694337323812b662580f6133442eb1c17885d0a2b5663cba9cadce4dabf5517dc34089b3399 + checksum: 56b5c3567f4b32d37faf93f69c7ca519a5300838065b896fdc291dbdd85e17551e00d44f536fb859e648ff2db2f1fb7176bc46aa2c44e5a947a3dd99b61f642e languageName: node linkType: hard -"@aws-sdk/util-stream-browser@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/util-stream-browser@npm:3.310.0" +"@aws-sdk/util-stream-browser@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/util-stream-browser@npm:3.329.0" dependencies: - "@aws-sdk/fetch-http-handler": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/fetch-http-handler": 3.329.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-base64": 3.310.0 "@aws-sdk/util-hex-encoding": 3.310.0 "@aws-sdk/util-utf8": 3.310.0 tslib: ^2.5.0 - checksum: 0058cdb410bd5d9365ce28c436dff17a534dfea38cc411f2bc83b34aa1be24850feae19f73b2fc7c2865df5d38d1f2502eb8ab4d061b3a90f4823bc09031f30c + checksum: a3451714c437c7a33aefa52e37182d58d7ff8e632f1243ec40bba6a352a7e9a7c58fc8cb2f4ba68b969aadbae08290e7506b8c0762080e3502427d8c747f7644 languageName: node linkType: hard -"@aws-sdk/util-stream-node@npm:3.310.0, @aws-sdk/util-stream-node@npm:^3.208.0": - version: 3.310.0 - resolution: "@aws-sdk/util-stream-node@npm:3.310.0" +"@aws-sdk/util-stream-node@npm:3.329.0, @aws-sdk/util-stream-node@npm:^3.310.0": + version: 3.329.0 + resolution: "@aws-sdk/util-stream-node@npm:3.329.0" dependencies: - "@aws-sdk/node-http-handler": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/node-http-handler": 3.329.0 + "@aws-sdk/types": 3.329.0 "@aws-sdk/util-buffer-from": 3.310.0 tslib: ^2.5.0 - checksum: 05caf55f81dd8151adccc23bc2f3f0007e095fb554bec279ec0c8045a7d8da22ecaed238f5d12705ae77e77b7cd81119bb5da2f1a9788962f5361647cafdf9ca + checksum: 67e018f8a9ad7da48ceaed48419580e017d91743de3872c9a23900e104651029261d70faed4b7cf479d2c0902297d3c7385ea3393af7798d2e6a88e037a259fc languageName: node linkType: hard @@ -1649,30 +1737,30 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.310.0" +"@aws-sdk/util-user-agent-browser@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.329.0" dependencies: - "@aws-sdk/types": 3.310.0 + "@aws-sdk/types": 3.329.0 bowser: ^2.11.0 tslib: ^2.5.0 - checksum: 32fc6249e762fcba3f3111ed627b644855e8127bc354911fdcdbd0332ea1915872bb0984f19c049fbc4feaf17e3bb02ff11b13d3792103ee8902d00c7fe3ff84 + checksum: ced136d39db2eaa62621819025df1c881e909e37826322ad5d5cb78d51d8e866f88ac1fe90857543a8f7f1fa1111d50bd369fa49495e465a167769935410df45 languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.310.0" +"@aws-sdk/util-user-agent-node@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.329.0" dependencies: - "@aws-sdk/node-config-provider": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/node-config-provider": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 peerDependencies: aws-crt: ">=1.0.0" peerDependenciesMeta: aws-crt: optional: true - checksum: 82d214f814405a538df8afb259f6a3f2d373cd87adbc2895ac93e9d1f4ed9f4f8f6dcc0ae8ba55887e99e45b5ea83c7b1e5ed3efccbcdbbcaee6a863a638d183 + checksum: 33fae730d0cdb0743af867373f58201c5aea8e48a0f449c9df19c08269cc18fc4b523e481895e4ba5b2a43446ec417f12691249e8b0339c30849473f5f36085f languageName: node linkType: hard @@ -1695,14 +1783,14 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-waiter@npm:3.310.0": - version: 3.310.0 - resolution: "@aws-sdk/util-waiter@npm:3.310.0" +"@aws-sdk/util-waiter@npm:3.329.0": + version: 3.329.0 + resolution: "@aws-sdk/util-waiter@npm:3.329.0" dependencies: - "@aws-sdk/abort-controller": 3.310.0 - "@aws-sdk/types": 3.310.0 + "@aws-sdk/abort-controller": 3.329.0 + "@aws-sdk/types": 3.329.0 tslib: ^2.5.0 - checksum: 51eb9fbb9a21cd02c312f4639e520b934e2d6ea93eb7c0c1c71f3cf415d61b0a681e7f88209f9e49f3e84bba83480d1c14b0c0b069b610bd12753ff1c1d68f42 + checksum: 41f8796c3649313dee3cbc5a643c9bc1d23a54e35a990c41875a3397f18c57844a68e2f0efb585548f88f6047b7e2c31db8c833481839788cee3024bc92aad81 languageName: node linkType: hard @@ -3384,7 +3472,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.21.0 resolution: "@babel/runtime@npm:7.21.0" dependencies: @@ -3508,11 +3596,11 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/backend-common@workspace:packages/backend-common" dependencies: - "@aws-sdk/abort-controller": ^3.208.0 - "@aws-sdk/client-s3": ^3.208.0 - "@aws-sdk/credential-providers": ^3.208.0 - "@aws-sdk/types": ^3.208.0 - "@aws-sdk/util-stream-node": ^3.208.0 + "@aws-sdk/abort-controller": ^3.310.0 + "@aws-sdk/client-s3": ^3.310.0 + "@aws-sdk/credential-providers": ^3.310.0 + "@aws-sdk/types": ^3.310.0 + "@aws-sdk/util-stream-node": ^3.310.0 "@backstage/backend-app-api": "workspace:^" "@backstage/backend-dev-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" @@ -4292,11 +4380,11 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/integration-aws-node@workspace:packages/integration-aws-node" dependencies: - "@aws-sdk/client-sts": ^3.208.0 - "@aws-sdk/credential-provider-node": ^3.208.0 - "@aws-sdk/credential-providers": ^3.208.0 - "@aws-sdk/types": ^3.208.0 - "@aws-sdk/util-arn-parser": ^3.208.0 + "@aws-sdk/client-sts": ^3.310.0 + "@aws-sdk/credential-provider-node": ^3.310.0 + "@aws-sdk/credential-providers": ^3.310.0 + "@aws-sdk/types": ^3.310.0 + "@aws-sdk/util-arn-parser": ^3.310.0 "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" @@ -4796,6 +4884,7 @@ __metadata: "@types/passport-strategy": ^0.2.35 "@types/xml2js": ^0.4.7 compression: ^1.7.4 + connect-session-knex: ^3.0.1 cookie-parser: ^1.4.5 cors: ^2.8.5 express: ^4.17.1 @@ -4986,18 +5075,24 @@ __metadata: resolution: "@backstage/plugin-badges-backend@workspace:plugins/badges-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" "@types/express": ^4.17.6 - "@types/supertest": ^2.0.8 + "@types/node": "*" badge-maker: ^3.3.0 cors: ^2.8.5 + cross-fetch: ^3.1.5 express: ^4.17.1 express-promise-router: ^4.1.0 - supertest: ^6.1.3 + knex: ^2.4.2 + lodash: ^4.17.21 + supertest: ^6.3.3 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -5020,14 +5115,9 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 - "@testing-library/dom": ^8.0.0 - "@testing-library/jest-dom": ^5.10.1 - "@testing-library/react": ^12.1.3 - "@testing-library/user-event": ^14.0.0 + "@testing-library/jest-dom": ^5.16.5 "@types/node": ^16.11.26 - "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 - msw: ^1.0.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5143,8 +5233,13 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws" dependencies: - "@aws-sdk/client-s3": ^3.208.0 - "@aws-sdk/util-stream-node": ^3.208.0 + "@aws-sdk/client-eks": ^3.310.0 + "@aws-sdk/client-organizations": ^3.310.0 + "@aws-sdk/client-s3": ^3.310.0 + "@aws-sdk/credential-providers": ^3.310.0 + "@aws-sdk/middleware-endpoint": ^3.310.0 + "@aws-sdk/types": ^3.310.0 + "@aws-sdk/util-stream-node": ^3.310.0 "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" @@ -5154,14 +5249,14 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/integration-aws-node": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/types": "workspace:^" "@types/lodash": ^4.14.151 - aws-sdk: ^2.840.0 aws-sdk-client-mock: ^2.0.0 - aws-sdk-mock: ^5.2.1 + aws-sdk-client-mock-jest: ^2.0.0 lodash: ^4.17.21 luxon: ^3.0.0 p-limit: ^3.0.2 @@ -6142,6 +6237,86 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-devtools-backend@workspace:^, @backstage/plugin-devtools-backend@workspace:plugins/devtools-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-devtools-backend@workspace:plugins/devtools-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/config-loader": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-devtools-common": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" + "@backstage/types": "workspace:^" + "@manypkg/get-packages": ^1.1.3 + "@types/express": "*" + "@types/minimist": ^1.2.0 + "@types/ping": ^0.4.1 + "@types/supertest": ^2.0.8 + "@types/yarnpkg__lockfile": ^1.1.4 + "@yarnpkg/lockfile": ^1.1.0 + "@yarnpkg/parsers": ^3.0.0-rc.4 + express: ^4.18.1 + express-promise-router: ^4.1.0 + fs-extra: ^10.0.0 + lodash: ^4.17.21 + msw: ^0.47.0 + node-fetch: ^2.6.7 + ping: ^0.4.1 + semver: ^7.3.2 + supertest: ^6.2.4 + winston: ^3.2.1 + yn: ^4.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-devtools-common@workspace:^, @backstage/plugin-devtools-common@workspace:plugins/devtools-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-devtools-common@workspace:plugins/devtools-common" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@backstage/types": "workspace:^" + languageName: unknown + linkType: soft + +"@backstage/plugin-devtools@workspace:^, @backstage/plugin-devtools@workspace:plugins/devtools": + version: 0.0.0-use.local + resolution: "@backstage/plugin-devtools@workspace:plugins/devtools" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-devtools-common": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@backstage/types": "workspace:^" + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + msw: ^0.47.0 + react-json-view: ^1.21.3 + react-use: ^17.2.4 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + languageName: unknown + linkType: soft + "@backstage/plugin-dynatrace@workspace:^, @backstage/plugin-dynatrace@workspace:plugins/dynatrace": version: 0.0.0-use.local resolution: "@backstage/plugin-dynatrace@workspace:plugins/dynatrace" @@ -6288,8 +6463,8 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-events-backend-module-aws-sqs@workspace:plugins/events-backend-module-aws-sqs" dependencies: - "@aws-sdk/client-sqs": ^3.208.0 - "@aws-sdk/types": ^3.208.0 + "@aws-sdk/client-sqs": ^3.310.0 + "@aws-sdk/types": ^3.310.0 "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" @@ -7050,7 +7225,6 @@ __metadata: cross-fetch: ^3.1.5 lodash: ^4.17.21 msw: ^1.0.0 - object-hash: ^3.0.0 react-grid-layout: ^1.3.4 react-resizable: ^3.0.4 react-use: ^17.2.4 @@ -7120,6 +7294,7 @@ __metadata: express-promise-router: ^4.1.0 jenkins: ^1.0.0 msw: ^1.0.0 + node-fetch: ^2.6.7 supertest: ^6.1.6 winston: ^3.2.1 yn: ^4.0.0 @@ -7235,7 +7410,7 @@ __metadata: resolution: "@backstage/plugin-kubernetes-backend@workspace:plugins/kubernetes-backend" dependencies: "@aws-crypto/sha256-js": ^3.0.0 - "@aws-sdk/credential-providers": ^3.208.0 + "@aws-sdk/credential-providers": ^3.310.0 "@aws-sdk/signature-v4": ^3.310.0 "@azure/identity": ^2.0.4 "@backstage/backend-common": "workspace:^" @@ -7259,7 +7434,6 @@ __metadata: "@types/express": ^4.17.6 "@types/http-proxy-middleware": ^0.19.3 "@types/luxon": ^3.0.0 - aws4: ^1.11.0 compression: ^1.7.4 cors: ^2.8.5 express: ^4.17.1 @@ -7637,9 +7811,12 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" + "@backstage/types": "workspace:^" "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 @@ -8100,6 +8277,7 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@gitbeaker/node": ^35.8.0 + zod: ^3.21.4 languageName: unknown linkType: soft @@ -8215,7 +8393,7 @@ __metadata: prom-client: ^14.0.1 supertest: ^6.1.3 uuid: ^8.2.0 - vm2: ^3.9.17 + vm2: ^3.9.18 wait-for-expect: ^3.0.2 winston: ^3.2.1 yaml: ^2.0.0 @@ -8406,14 +8584,14 @@ __metadata: "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/integration-aws-node": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@elastic/elasticsearch": ^7.13.0 "@elastic/elasticsearch-mock": ^1.0.0 - "@opensearch-project/opensearch": ^2.0.0 + "@opensearch-project/opensearch": ^2.2.1 "@short.io/opensearch-mock": ^0.3.1 - aws-os-connection: ^0.2.0 - aws-sdk: ^2.948.0 + aws4: ^1.12.0 elastic-builder: ^2.16.0 lodash: ^4.17.21 uuid: ^8.3.2 @@ -8509,6 +8687,7 @@ __metadata: resolution: "@backstage/plugin-search-backend@workspace:plugins/search-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-openapi-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -9138,11 +9317,11 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-techdocs-node@workspace:plugins/techdocs-node" dependencies: - "@aws-sdk/client-s3": ^3.208.0 - "@aws-sdk/credential-providers": ^3.208.0 - "@aws-sdk/lib-storage": ^3.208.0 - "@aws-sdk/node-http-handler": ^3.208.0 - "@aws-sdk/types": ^3.208.0 + "@aws-sdk/client-s3": ^3.310.0 + "@aws-sdk/credential-providers": ^3.310.0 + "@aws-sdk/lib-storage": ^3.310.0 + "@aws-sdk/node-http-handler": ^3.310.0 + "@aws-sdk/types": ^3.310.0 "@azure/identity": ^2.1.0 "@azure/storage-blob": ^12.5.0 "@backstage/backend-common": "workspace:^" @@ -9264,6 +9443,7 @@ __metadata: resolution: "@backstage/plugin-todo-backend@workspace:plugins/todo-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-openapi-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" @@ -9552,7 +9732,19 @@ __metadata: languageName: unknown linkType: soft -"@backstage/theme@^0.2.16, @backstage/theme@^0.2.18, @backstage/theme@^0.2.19, @backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": +"@backstage/theme@npm:^0.2.16, @backstage/theme@npm:^0.2.18, @backstage/theme@npm:^0.2.19": + version: 0.2.19 + resolution: "@backstage/theme@npm:0.2.19" + dependencies: + "@material-ui/core": ^4.12.2 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + react-dom: ^16.13.1 || ^17.0.0 + checksum: 93ca110a3f953d6d98b4a5f2163beaa1bd1f95adb66a593e85eb523a7012acf654b6148db54008a6434805a4bd9016d67e5c6a317bc56940ee6800aecbb97d6f + languageName: node + linkType: hard + +"@backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": version: 0.0.0-use.local resolution: "@backstage/theme@workspace:packages/theme" dependencies: @@ -10151,9 +10343,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/android-arm64@npm:0.17.18" +"@esbuild/android-arm64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/android-arm64@npm:0.17.19" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -10172,9 +10364,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/android-arm@npm:0.17.18" +"@esbuild/android-arm@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/android-arm@npm:0.17.19" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -10186,9 +10378,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/android-x64@npm:0.17.18" +"@esbuild/android-x64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/android-x64@npm:0.17.19" conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -10200,9 +10392,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/darwin-arm64@npm:0.17.18" +"@esbuild/darwin-arm64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/darwin-arm64@npm:0.17.19" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -10214,9 +10406,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/darwin-x64@npm:0.17.18" +"@esbuild/darwin-x64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/darwin-x64@npm:0.17.19" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -10228,9 +10420,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/freebsd-arm64@npm:0.17.18" +"@esbuild/freebsd-arm64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/freebsd-arm64@npm:0.17.19" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -10242,9 +10434,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/freebsd-x64@npm:0.17.18" +"@esbuild/freebsd-x64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/freebsd-x64@npm:0.17.19" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -10256,9 +10448,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/linux-arm64@npm:0.17.18" +"@esbuild/linux-arm64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/linux-arm64@npm:0.17.19" conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -10270,9 +10462,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/linux-arm@npm:0.17.18" +"@esbuild/linux-arm@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/linux-arm@npm:0.17.19" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -10284,9 +10476,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/linux-ia32@npm:0.17.18" +"@esbuild/linux-ia32@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/linux-ia32@npm:0.17.19" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -10305,9 +10497,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/linux-loong64@npm:0.17.18" +"@esbuild/linux-loong64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/linux-loong64@npm:0.17.19" conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -10319,9 +10511,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/linux-mips64el@npm:0.17.18" +"@esbuild/linux-mips64el@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/linux-mips64el@npm:0.17.19" conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -10333,9 +10525,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/linux-ppc64@npm:0.17.18" +"@esbuild/linux-ppc64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/linux-ppc64@npm:0.17.19" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -10347,9 +10539,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/linux-riscv64@npm:0.17.18" +"@esbuild/linux-riscv64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/linux-riscv64@npm:0.17.19" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -10361,9 +10553,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/linux-s390x@npm:0.17.18" +"@esbuild/linux-s390x@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/linux-s390x@npm:0.17.19" conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -10375,9 +10567,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/linux-x64@npm:0.17.18" +"@esbuild/linux-x64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/linux-x64@npm:0.17.19" conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -10389,9 +10581,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/netbsd-x64@npm:0.17.18" +"@esbuild/netbsd-x64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/netbsd-x64@npm:0.17.19" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -10403,9 +10595,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/openbsd-x64@npm:0.17.18" +"@esbuild/openbsd-x64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/openbsd-x64@npm:0.17.19" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard @@ -10417,9 +10609,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/sunos-x64@npm:0.17.18" +"@esbuild/sunos-x64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/sunos-x64@npm:0.17.19" conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -10431,9 +10623,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/win32-arm64@npm:0.17.18" +"@esbuild/win32-arm64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/win32-arm64@npm:0.17.19" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -10445,9 +10637,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/win32-ia32@npm:0.17.18" +"@esbuild/win32-ia32@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/win32-ia32@npm:0.17.19" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -10459,9 +10651,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.17.18": - version: 0.17.18 - resolution: "@esbuild/win32-x64@npm:0.17.18" +"@esbuild/win32-x64@npm:0.17.19": + version: 0.17.19 + resolution: "@esbuild/win32-x64@npm:0.17.19" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -12094,11 +12286,11 @@ __metadata: linkType: hard "@keyv/redis@npm:^2.5.3": - version: 2.5.7 - resolution: "@keyv/redis@npm:2.5.7" + version: 2.5.8 + resolution: "@keyv/redis@npm:2.5.8" dependencies: - ioredis: ^5.3.1 - checksum: e0ed1b6602afcc339bb2bf014d8801e6b7a3b6392d0f8e4ccfc59d919f59229b8787c50c48fc51636625ffdf55cf1ef6c450425a73bd88f9b866e74f1b3e25f8 + ioredis: ^5.3.2 + checksum: 1050c4997b62e701112b48e5375c4e53a5e49454c1621fbb00b1c2272e55a575f9ace49c5179800f73e6f07ce247a4cf6e1260a6713ad1392fd8f8825079771f languageName: node linkType: hard @@ -13447,7 +13639,7 @@ __metadata: languageName: node linkType: hard -"@opensearch-project/opensearch@npm:^2.0.0": +"@opensearch-project/opensearch@npm:^2.2.1": version: 2.2.1 resolution: "@opensearch-project/opensearch@npm:2.2.1" dependencies: @@ -13835,8 +14027,8 @@ __metadata: linkType: hard "@roadiehq/backstage-plugin-github-pull-requests@npm:^2.2.7": - version: 2.5.9 - resolution: "@roadiehq/backstage-plugin-github-pull-requests@npm:2.5.9" + version: 2.5.10 + resolution: "@roadiehq/backstage-plugin-github-pull-requests@npm:2.5.10" dependencies: "@backstage/catalog-model": ^1.3.0 "@backstage/core-components": ^0.13.0 @@ -13859,7 +14051,7 @@ __metadata: react: ^16.13.1 || ^17.0.0 react-dom: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 - checksum: 5a60f2c67c095a0fbf081b4097e0dfd4d6c22f59514e66800eba8460ef32ee43cf1eb7105a36a56f7b63d8ac44f5343f7e4ec66279ba2009945b828e6698e00e + checksum: 0f273ede63024635f850fc9df662941589f5f8848307507e3f6e376ea0bd9b7e17026e1182808d5c193be90ee9a2725c83a0850475547b364cd3d7cf35bf7b74 languageName: node linkType: hard @@ -15000,7 +15192,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:^5.10.1, @testing-library/jest-dom@npm:^5.16.4": +"@testing-library/jest-dom@npm:^5.10.1, @testing-library/jest-dom@npm:^5.16.4, @testing-library/jest-dom@npm:^5.16.5": version: 5.16.5 resolution: "@testing-library/jest-dom@npm:5.16.5" dependencies: @@ -15582,12 +15774,12 @@ __metadata: linkType: hard "@types/dockerode@npm:^3.3.0, @types/dockerode@npm:^3.3.8": - version: 3.3.16 - resolution: "@types/dockerode@npm:3.3.16" + version: 3.3.17 + resolution: "@types/dockerode@npm:3.3.17" dependencies: "@types/docker-modem": "*" "@types/node": "*" - checksum: ef316e330f8a1a137962d6000cac53bb25b5ba4300e38dd5842f32a6c58bec420278d61526c68865351c48c7e843c9aa45bcbca332737630a0c251e09390bf5b + checksum: 392d7bdfa15659941a50260f91110b9332fa1860c3ca258e08959d7a2624520bd64c5bb20fa75940a87b0c7586e4e40bae05f23c79976375b58031205493acba languageName: node linkType: hard @@ -15656,14 +15848,14 @@ __metadata: linkType: hard "@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.30, @types/express-serve-static-core@npm:^4.17.33, @types/express-serve-static-core@npm:^4.17.5": - version: 4.17.34 - resolution: "@types/express-serve-static-core@npm:4.17.34" + version: 4.17.35 + resolution: "@types/express-serve-static-core@npm:4.17.35" dependencies: "@types/node": "*" "@types/qs": "*" "@types/range-parser": "*" "@types/send": "*" - checksum: 3b5242e7d6cfecca5300635fd2af0f63aca3a92754da79a4a355c4d85b57099aa2cabb1c8557fc38a8a9e6f0be996339140ad017e5be405ea1b877a8294a136d + checksum: cc8995d10c6feda475ec1b3a0e69eb0f35f21ab6b49129ad5c6f279e0bc5de8175bc04ec51304cb79a43eec3ed2f5a1e01472eb6d5f827b8c35c6ca8ad24eb6e languageName: node linkType: hard @@ -16212,10 +16404,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^18.11.17": - version: 18.16.3 - resolution: "@types/node@npm:18.16.3" - checksum: 816b39d45b05ebdc6f362b630970df3f6d82f71d418a2555353522f4eeeb078fa201de5299f02c09a09faa975e43b2745fe19c263d44069f87ddf37d6c37b717 +"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": + version: 20.1.4 + resolution: "@types/node@npm:20.1.4" + checksum: bffa6a6e92831cc805b13658c55be8e04abe3202d5e57ddd7fee4cfbe269656a1f475909a9f9514e1aff50310d0d90c516ac2c649cb6f1c77d4b1079c2fd1e88 languageName: node linkType: hard @@ -16234,9 +16426,9 @@ __metadata: linkType: hard "@types/node@npm:^14.14.31": - version: 14.18.43 - resolution: "@types/node@npm:14.18.43" - checksum: 3ef43eda265c346597bc5c8329605255550f7a92300a76e9e3c48e07fd685b4a48d47465997bf6f1a68b235c7339e6e583d73515769efcad70e4f8f4c7f47fd4 + version: 14.18.47 + resolution: "@types/node@npm:14.18.47" + checksum: 249467fa37e60bb7ce631616a5fea53f3b957a6792c6a09f7db7cdff85a7410c9e657dd9fa54bcc9d878e58f35407ce35169b84556f46b2154f90bc71063e1d0 languageName: node linkType: hard @@ -16248,9 +16440,16 @@ __metadata: linkType: hard "@types/node@npm:^16.0.0, @types/node@npm:^16.11.26, @types/node@npm:^16.9.2": - version: 16.18.25 - resolution: "@types/node@npm:16.18.25" - checksum: 181760ad6b54fcc498dfeb249e98bbf0be51d7c35e92e760e1a82004fa42b86e8c33a8f8dd7743b5ef872bda0753d9e6a5b8e3f0aed63e9eb79b4e65760c1fbe + version: 16.18.30 + resolution: "@types/node@npm:16.18.30" + checksum: 2975519e337a3f29c495b97aa7510ef3f4ac5d3c9356f7886698a4abf8b12641c1f1247512b525599cba6aefe774735ef25fc83555e989033bdbcc4a2b1c7401 + languageName: node + linkType: hard + +"@types/node@npm:^18.11.17": + version: 18.16.9 + resolution: "@types/node@npm:18.16.9" + checksum: 7fe0ef372ba8b157bd646efe29b0ece92ccb9f455171a7ceb4c92d6b490789ff0f14411b1dc6ad203351a0efa2b435072dfab5566fb6eaaaa5cd549e1fb22afa languageName: node linkType: hard @@ -16383,6 +16582,13 @@ __metadata: languageName: node linkType: hard +"@types/ping@npm:^0.4.1": + version: 0.4.1 + resolution: "@types/ping@npm:0.4.1" + checksum: 9b94837fe66df70558c5a42b0e0c8371b4950ab56b96c42c8df809ff2cf52477dd0a7e01d2e6b38af8bb6683b3dcb54587960b96b4b1f3d40fdb529aea348ad0 + languageName: node + linkType: hard + "@types/pluralize@npm:^0.0.29": version: 0.0.29 resolution: "@types/pluralize@npm:0.0.29" @@ -16848,12 +17054,12 @@ __metadata: linkType: hard "@types/tar@npm:^6.1.1": - version: 6.1.4 - resolution: "@types/tar@npm:6.1.4" + version: 6.1.5 + resolution: "@types/tar@npm:6.1.5" dependencies: "@types/node": "*" minipass: ^4.0.0 - checksum: 48a1f6b8665ed03f37f74966cfe991a33d6e85cee28aaa6ef42c7ca8448cdbc1e2ca96b473b6b53f1d7bd91423ae0dcfe54a295607d7ac4b4cbb9dbf167df7fa + checksum: 1efa71c8d72f2b02d16ffecb65c4a96be9dacce000c4207532a686477386524dd99b6e26904319354be3ac9ac4d41296a0cb9cf87d05f8f21ae31c68fe446348 languageName: node linkType: hard @@ -18377,18 +18583,6 @@ __metadata: languageName: node linkType: hard -"aws-os-connection@npm:^0.2.0": - version: 0.2.0 - resolution: "aws-os-connection@npm:0.2.0" - dependencies: - aws4: ^1.11.0 - peerDependencies: - "@opensearch-project/opensearch": 2 - aws-sdk: ">=2" - checksum: 1708ffd9bdc5f7bd6b8447f97859936a8657f4df567f61f12dacd040e6f1a25020bbea8027bb28f19fe27d171a0d3a5090c2d84d6f752bb04e387640413fec7c - languageName: node - linkType: hard - "aws-sdk-client-mock-jest@npm:^2.0.0": version: 2.1.1 resolution: "aws-sdk-client-mock-jest@npm:2.1.1" @@ -18423,7 +18617,7 @@ __metadata: languageName: node linkType: hard -"aws-sdk@npm:^2.1231.0, aws-sdk@npm:^2.840.0, aws-sdk@npm:^2.948.0": +"aws-sdk@npm:^2.1231.0": version: 2.1279.0 resolution: "aws-sdk@npm:2.1279.0" dependencies: @@ -18448,7 +18642,7 @@ __metadata: languageName: node linkType: hard -"aws4@npm:^1.11.0, aws4@npm:^1.8.0": +"aws4@npm:^1.11.0, aws4@npm:^1.12.0, aws4@npm:^1.8.0": version: 1.12.0 resolution: "aws4@npm:1.12.0" checksum: 68f79708ac7c335992730bf638286a3ee0a645cf12575d557860100767c500c08b30e24726b9f03265d74116417f628af78509e1333575e9f8d52a80edfe8cbc @@ -18784,6 +18978,13 @@ __metadata: languageName: node linkType: hard +"base16@npm:^1.0.0": + version: 1.0.0 + resolution: "base16@npm:1.0.0" + checksum: 0cd449a2db0f0f957e4b6b57e33bc43c9e20d4f1dd744065db94b5da35e8e71fa4dc4bc7a901e59a84d5f8b6936e3c520e2471787f667fc155fb0f50d8540f5d + languageName: node + linkType: hard + "base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -20535,6 +20736,16 @@ __metadata: languageName: node linkType: hard +"connect-session-knex@npm:^3.0.1": + version: 3.0.1 + resolution: "connect-session-knex@npm:3.0.1" + dependencies: + bluebird: ^3.7.2 + knex: ^2.3.0 + checksum: f5a80c3c34d30e7cd4e79a6aae09d112825986d51ddc3a4563ef95ade425178239d4e3e05420fb3b6204b2353b0dfd754d318b288989b539583f6c0e52758b7a + languageName: node + linkType: hard + "consola@npm:^2.15.0": version: 2.15.3 resolution: "consola@npm:2.15.3" @@ -20660,7 +20871,7 @@ __metadata: languageName: node linkType: hard -"cookiejar@npm:^2.1.3": +"cookiejar@npm:^2.1.4": version: 2.1.4 resolution: "cookiejar@npm:2.1.4" checksum: c4442111963077dc0e5672359956d6556a195d31cbb35b528356ce5f184922b99ac48245ac05ed86cf993f7df157c56da10ab3efdadfed79778a0d9b1b092d5b @@ -20882,7 +21093,7 @@ __metadata: languageName: node linkType: hard -"cross-fetch@npm:3.1.5, cross-fetch@npm:^3.0.4, cross-fetch@npm:^3.1.3, cross-fetch@npm:^3.1.5": +"cross-fetch@npm:3.1.5": version: 3.1.5 resolution: "cross-fetch@npm:3.1.5" dependencies: @@ -20891,6 +21102,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^3.1.3, cross-fetch@npm:^3.1.5": + version: 3.1.6 + resolution: "cross-fetch@npm:3.1.6" + dependencies: + node-fetch: ^2.6.11 + checksum: 704b3519ab7de488328cc49a52cf1aa14132ec748382be5b9557b22398c33ffa7f8c2530e8a97ed8cb55da52b0a9740a9791d361271c4591910501682d981d9c + languageName: node + linkType: hard + "cross-spawn@npm:^5.1.0": version: 5.1.0 resolution: "cross-spawn@npm:5.1.0" @@ -22867,31 +23087,31 @@ __metadata: linkType: hard "esbuild@npm:^0.17.0": - version: 0.17.18 - resolution: "esbuild@npm:0.17.18" + version: 0.17.19 + resolution: "esbuild@npm:0.17.19" dependencies: - "@esbuild/android-arm": 0.17.18 - "@esbuild/android-arm64": 0.17.18 - "@esbuild/android-x64": 0.17.18 - "@esbuild/darwin-arm64": 0.17.18 - "@esbuild/darwin-x64": 0.17.18 - "@esbuild/freebsd-arm64": 0.17.18 - "@esbuild/freebsd-x64": 0.17.18 - "@esbuild/linux-arm": 0.17.18 - "@esbuild/linux-arm64": 0.17.18 - "@esbuild/linux-ia32": 0.17.18 - "@esbuild/linux-loong64": 0.17.18 - "@esbuild/linux-mips64el": 0.17.18 - "@esbuild/linux-ppc64": 0.17.18 - "@esbuild/linux-riscv64": 0.17.18 - "@esbuild/linux-s390x": 0.17.18 - "@esbuild/linux-x64": 0.17.18 - "@esbuild/netbsd-x64": 0.17.18 - "@esbuild/openbsd-x64": 0.17.18 - "@esbuild/sunos-x64": 0.17.18 - "@esbuild/win32-arm64": 0.17.18 - "@esbuild/win32-ia32": 0.17.18 - "@esbuild/win32-x64": 0.17.18 + "@esbuild/android-arm": 0.17.19 + "@esbuild/android-arm64": 0.17.19 + "@esbuild/android-x64": 0.17.19 + "@esbuild/darwin-arm64": 0.17.19 + "@esbuild/darwin-x64": 0.17.19 + "@esbuild/freebsd-arm64": 0.17.19 + "@esbuild/freebsd-x64": 0.17.19 + "@esbuild/linux-arm": 0.17.19 + "@esbuild/linux-arm64": 0.17.19 + "@esbuild/linux-ia32": 0.17.19 + "@esbuild/linux-loong64": 0.17.19 + "@esbuild/linux-mips64el": 0.17.19 + "@esbuild/linux-ppc64": 0.17.19 + "@esbuild/linux-riscv64": 0.17.19 + "@esbuild/linux-s390x": 0.17.19 + "@esbuild/linux-x64": 0.17.19 + "@esbuild/netbsd-x64": 0.17.19 + "@esbuild/openbsd-x64": 0.17.19 + "@esbuild/sunos-x64": 0.17.19 + "@esbuild/win32-arm64": 0.17.19 + "@esbuild/win32-ia32": 0.17.19 + "@esbuild/win32-x64": 0.17.19 dependenciesMeta: "@esbuild/android-arm": optional: true @@ -22939,7 +23159,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 900b333f649fd89804216fb61fb5a0ffadc6dc37a2ec3b5981b588f72821676ea649a7c0ec785f0dbe6e774080b084c8af5f6ee7adbc1b138faf2a8c35e2c69c + checksum: ac11b1a5a6008e4e37ccffbd6c2c054746fc58d0ed4a2f9ee643bd030cfcea9a33a235087bc777def8420f2eaafb3486e76adb7bdb7241a9143b43a69a10afd8 languageName: node linkType: hard @@ -23618,6 +23838,7 @@ __metadata: "@backstage/plugin-cloudbuild": "workspace:^" "@backstage/plugin-code-coverage": "workspace:^" "@backstage/plugin-cost-insights": "workspace:^" + "@backstage/plugin-devtools": "workspace:^" "@backstage/plugin-dynatrace": "workspace:^" "@backstage/plugin-entity-feedback": "workspace:^" "@backstage/plugin-explore": "workspace:^" @@ -23741,6 +23962,7 @@ __metadata: "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-code-coverage-backend": "workspace:^" + "@backstage/plugin-devtools-backend": "workspace:^" "@backstage/plugin-entity-feedback-backend": "workspace:^" "@backstage/plugin-events-backend": "workspace:^" "@backstage/plugin-events-node": "workspace:^" @@ -24222,6 +24444,15 @@ __metadata: languageName: node linkType: hard +"fbemitter@npm:^3.0.0": + version: 3.0.0 + resolution: "fbemitter@npm:3.0.0" + dependencies: + fbjs: ^3.0.0 + checksum: 069690b8cdff3521ade3c9beb92ba0a38d818a86ef36dff8690e66749aef58809db4ac0d6938eb1cacea2dbef5f2a508952d455669590264cdc146bbe839f605 + languageName: node + linkType: hard + "fbjs-css-vars@npm:^1.0.0": version: 1.0.2 resolution: "fbjs-css-vars@npm:1.0.2" @@ -24244,18 +24475,18 @@ __metadata: languageName: node linkType: hard -"fbjs@npm:^3.0.0": - version: 3.0.0 - resolution: "fbjs@npm:3.0.0" +"fbjs@npm:^3.0.0, fbjs@npm:^3.0.1": + version: 3.0.4 + resolution: "fbjs@npm:3.0.4" dependencies: - cross-fetch: ^3.0.4 + cross-fetch: ^3.1.5 fbjs-css-vars: ^1.0.0 loose-envify: ^1.0.0 object-assign: ^4.1.0 promise: ^7.1.1 setimmediate: ^1.0.5 - ua-parser-js: ^0.7.18 - checksum: 85ec57d8dbeddd7c82bf8f111a3c7de1abc1f4d7c603d6ccbcc1ec8dce35ff5b7a113dd34acbf7930093e5533c37a2298a92d342077f967bef34dc7cf2f3f07e + ua-parser-js: ^0.7.30 + checksum: 8b23a3550fcda8a9109fca9475a3416590c18bb6825ea884192864ed686f67fcd618e308a140c9e5444fbd0168732e1ff3c092ba3d0c0ae1768969f32ba280c7 languageName: node linkType: hard @@ -24499,6 +24730,18 @@ __metadata: languageName: node linkType: hard +"flux@npm:^4.0.1": + version: 4.0.4 + resolution: "flux@npm:4.0.4" + dependencies: + fbemitter: ^3.0.0 + fbjs: ^3.0.1 + peerDependencies: + react: ^15.0.2 || ^16.0.0 || ^17.0.0 + checksum: 8fa5c2f9322258de3e331f67c6f1078a7f91c4dec9dbe8a54c4b8a80eed19a4f91889028b768668af4a796e8f2ee75e461e1571b8615432a3920ae95cc4ff794 + languageName: node + linkType: hard + "fn.name@npm:1.x.x": version: 1.1.0 resolution: "fn.name@npm:1.1.0" @@ -24665,7 +24908,7 @@ __metadata: languageName: node linkType: hard -"formidable@npm:^2.0.1": +"formidable@npm:^2.1.2": version: 2.1.2 resolution: "formidable@npm:2.1.2" dependencies: @@ -26532,9 +26775,9 @@ __metadata: languageName: node linkType: hard -"ioredis@npm:^5.3.1": - version: 5.3.1 - resolution: "ioredis@npm:5.3.1" +"ioredis@npm:^5.3.2": + version: 5.3.2 + resolution: "ioredis@npm:5.3.2" dependencies: "@ioredis/commands": ^1.1.1 cluster-key-slot: ^1.1.0 @@ -26545,7 +26788,7 @@ __metadata: redis-errors: ^1.2.0 redis-parser: ^3.0.0 standard-as-callback: ^2.1.0 - checksum: 6c98cb8f8772ad4bb2b6b7a0a224e4a63f4759f9a28e84205f18788552f08221d51f391c10892dca01a1b10b2804d0a7e6082b0b7decd65538a5fb6b0f4f1f82 + checksum: 9a23559133e862a768778301efb68ae8c2af3c33562174b54a4c2d6574b976e85c75a4c34857991af733e35c48faf4c356e7daa8fb0a3543d85ff1768c8754bc languageName: node linkType: hard @@ -28973,7 +29216,7 @@ __metadata: languageName: node linkType: hard -"knex@npm:^2.0.0": +"knex@npm:^2.0.0, knex@npm:^2.3.0, knex@npm:^2.4.2": version: 2.4.2 resolution: "knex@npm:2.4.2" dependencies: @@ -29439,6 +29682,13 @@ __metadata: languageName: node linkType: hard +"lodash.curry@npm:^4.0.1": + version: 4.1.1 + resolution: "lodash.curry@npm:4.1.1" + checksum: 9192b70fe7df4d1ff780c0260bee271afa9168c93fe4fa24bc861900240531b59781b5fdaadf4644fea8f4fbcd96f0700539ab294b579ffc1022c6c15dcc462a + languageName: node + linkType: hard + "lodash.debounce@npm:^4, lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -29474,6 +29724,13 @@ __metadata: languageName: node linkType: hard +"lodash.flow@npm:^3.3.0": + version: 3.5.0 + resolution: "lodash.flow@npm:3.5.0" + checksum: a9a62ad344e3c5a1f42bc121da20f64dd855aaafecee24b1db640f29b88bd165d81c37ff7e380a7191de6f70b26f5918abcebbee8396624f78f3618a0b18634c + languageName: node + linkType: hard + "lodash.get@npm:^4.4.2": version: 4.4.2 resolution: "lodash.get@npm:4.4.2" @@ -30968,6 +31225,13 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea + languageName: node + linkType: hard + "minizlib@npm:^2.0.0, minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -31083,6 +31347,41 @@ __metadata: languageName: node linkType: hard +"msw@npm:^0.47.0": + version: 0.47.4 + resolution: "msw@npm:0.47.4" + dependencies: + "@mswjs/cookies": ^0.2.2 + "@mswjs/interceptors": ^0.17.5 + "@open-draft/until": ^1.0.3 + "@types/cookie": ^0.4.1 + "@types/js-levenshtein": ^1.1.1 + chalk: 4.1.1 + chokidar: ^3.4.2 + cookie: ^0.4.2 + graphql: ^15.0.0 || ^16.0.0 + headers-polyfill: ^3.1.0 + inquirer: ^8.2.0 + is-node-process: ^1.0.1 + js-levenshtein: ^1.1.6 + node-fetch: ^2.6.7 + outvariant: ^1.3.0 + path-to-regexp: ^6.2.0 + statuses: ^2.0.0 + strict-event-emitter: ^0.2.6 + type-fest: ^2.19.0 + yargs: ^17.3.1 + peerDependencies: + typescript: ">= 4.2.x <= 4.8.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 10ff632641d40384d6622abf4df6399e4ae649db0f676b5d1ee2d0a515ec96f33abe9d4fecba08cdba4b2e43255af419da9eefc020d40a7e10669d0906457197 + languageName: node + linkType: hard + "msw@npm:^0.49.0": version: 0.49.3 resolution: "msw@npm:0.49.3" @@ -31254,12 +31553,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.1.23, nanoid@npm:^3.3.4": - version: 3.3.4 - resolution: "nanoid@npm:3.3.4" +"nanoid@npm:^3.1.23, nanoid@npm:^3.3.4, nanoid@npm:^3.3.6": + version: 3.3.6 + resolution: "nanoid@npm:3.3.6" bin: nanoid: bin/nanoid.cjs - checksum: 2fddd6dee994b7676f008d3ffa4ab16035a754f4bb586c61df5a22cf8c8c94017aadd360368f47d653829e0569a92b129979152ff97af23a558331e47e37cd9c + checksum: 7d0eda657002738aa5206107bd0580aead6c95c460ef1bdd0b1a87a9c7ae6277ac2e9b945306aaa5b32c6dcb7feaf462d0f552e7f8b5718abfc6ead5c94a71b3 languageName: node linkType: hard @@ -31435,9 +31734,9 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7": - version: 2.6.9 - resolution: "node-fetch@npm:2.6.9" +"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7": + version: 2.6.11 + resolution: "node-fetch@npm:2.6.11" dependencies: whatwg-url: ^5.0.0 peerDependencies: @@ -31445,7 +31744,7 @@ __metadata: peerDependenciesMeta: encoding: optional: true - checksum: acb04f9ce7224965b2b59e71b33c639794d8991efd73855b0b250921382b38331ffc9d61bce502571f6cc6e11a8905ca9b1b6d4aeb586ab093e2756a1fd190d0 + checksum: 249d0666a9497553384d46b5ab296ba223521ac88fed4d8a17d6ee6c2efb0fc890f3e8091cafe7f9fba8151a5b8d925db2671543b3409a56c3cd522b468b47b3 languageName: node linkType: hard @@ -33154,6 +33453,13 @@ __metadata: languageName: node linkType: hard +"ping@npm:^0.4.1": + version: 0.4.4 + resolution: "ping@npm:0.4.4" + checksum: cab10af309312e3a6822eccb7f323f0c9a1e17ef5895954114e31b6a1cced5e2ced3563990b79c66d074372e75aa4c846d9001120ee296b365de6a2adef5b8f9 + languageName: node + linkType: hard + "pinkie-promise@npm:^2.0.0": version: 2.0.1 resolution: "pinkie-promise@npm:2.0.1" @@ -33708,13 +34014,13 @@ __metadata: linkType: hard "postcss@npm:^8.1.0, postcss@npm:^8.4.19": - version: 8.4.21 - resolution: "postcss@npm:8.4.21" + version: 8.4.23 + resolution: "postcss@npm:8.4.23" dependencies: - nanoid: ^3.3.4 + nanoid: ^3.3.6 picocolors: ^1.0.0 source-map-js: ^1.0.2 - checksum: e39ac60ccd1542d4f9d93d894048aac0d686b3bb38e927d8386005718e6793dbbb46930f0a523fe382f1bbd843c6d980aaea791252bf5e176180e5a4336d9679 + checksum: 8bb9d1b2ea6e694f8987d4f18c94617971b2b8d141602725fedcc2222fdc413b776a6e1b969a25d627d7b2681ca5aabb56f59e727ef94072e1b6ac8412105a2f languageName: node linkType: hard @@ -34242,6 +34548,13 @@ __metadata: languageName: node linkType: hard +"pure-color@npm:^1.2.0": + version: 1.3.0 + resolution: "pure-color@npm:1.3.0" + checksum: 646d8bed6e6eab89affdd5e2c11f607a85b631a7fb03c061dfa658eb4dc4806881a15feed2ac5fd8c0bad8c00c632c640d5b1cb8b9a972e6e947393a1329371b + languageName: node + linkType: hard + "pvtsutils@npm:^1.3.2": version: 1.3.2 resolution: "pvtsutils@npm:1.3.2" @@ -34258,7 +34571,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.11.0, qs@npm:^6.10.1, qs@npm:^6.10.2, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.9.1, qs@npm:^6.9.4, qs@npm:^6.9.6": +"qs@npm:6.11.0, qs@npm:^6.10.1, qs@npm:^6.10.2, qs@npm:^6.11.0, qs@npm:^6.9.1, qs@npm:^6.9.4, qs@npm:^6.9.6": version: 6.11.0 resolution: "qs@npm:6.11.0" dependencies: @@ -34474,6 +34787,18 @@ __metadata: languageName: node linkType: hard +"react-base16-styling@npm:^0.6.0": + version: 0.6.0 + resolution: "react-base16-styling@npm:0.6.0" + dependencies: + base16: ^1.0.0 + lodash.curry: ^4.0.1 + lodash.flow: ^3.3.0 + pure-color: ^1.2.0 + checksum: 00a12dddafc8a9025cca933b0dcb65fca41c81fa176d1fc3a6a9d0242127042e2c0a604f4c724a3254dd2c6aeb5ef55095522ff22f5462e419641c1341a658e4 + languageName: node + linkType: hard + "react-beautiful-dnd@npm:^13.0.0": version: 13.0.0 resolution: "react-beautiful-dnd@npm:13.0.0" @@ -34743,6 +35068,21 @@ __metadata: languageName: node linkType: hard +"react-json-view@npm:^1.21.3": + version: 1.21.3 + resolution: "react-json-view@npm:1.21.3" + dependencies: + flux: ^4.0.1 + react-base16-styling: ^0.6.0 + react-lifecycles-compat: ^3.0.4 + react-textarea-autosize: ^8.3.2 + peerDependencies: + react: ^17.0.0 || ^16.3.0 || ^15.5.4 + react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4 + checksum: 5718bcd9210ad5b06eb9469cf8b9b44be9498845a7702e621343618e8251f26357e6e1c865532cf170db6165df1cb30202787e057309d8848c220bc600ec0d1a + languageName: node + linkType: hard + "react-lifecycles-compat@npm:^3.0.2, react-lifecycles-compat@npm:^3.0.4": version: 3.0.4 resolution: "react-lifecycles-compat@npm:3.0.4" @@ -35010,6 +35350,19 @@ __metadata: languageName: node linkType: hard +"react-textarea-autosize@npm:^8.3.2": + version: 8.4.1 + resolution: "react-textarea-autosize@npm:8.4.1" + dependencies: + "@babel/runtime": ^7.20.13 + use-composed-ref: ^1.3.0 + use-latest: ^1.2.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: b200437cd68938c23b13944fe6fdfeb32a6d949ac88588307f14d6fcdaba3044b8c7d8e239851b081f2101d433b93d4cf5aa027543b170b84f2a0cbe6fc9093f + languageName: node + linkType: hard + "react-transition-group@npm:2.9.0, react-transition-group@npm:^2.2.1": version: 2.9.0 resolution: "react-transition-group@npm:2.9.0" @@ -35076,12 +35429,12 @@ __metadata: linkType: hard "react-virtualized-auto-sizer@npm:^1.0.11, react-virtualized-auto-sizer@npm:^1.0.6": - version: 1.0.15 - resolution: "react-virtualized-auto-sizer@npm:1.0.15" + version: 1.0.16 + resolution: "react-virtualized-auto-sizer@npm:1.0.16" peerDependencies: react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc - checksum: db6716fff307dcd2b2492ee0539d5fe2922ea067f91a69969aed6ac03f46db105e993e0a9f946e74b1fb3b9caf88ce5629bf1132e3a995457f42b559979d6708 + checksum: 68319e26b162dd3f5186ed88f3977658a9da5963a250567d7fe0cd44671a87b765652248d3c39e1f02aa46c22df51a34748e11f436a70296b37ee509be89dab6 languageName: node linkType: hard @@ -37374,7 +37727,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:2.0.1": +"statuses@npm:2.0.1, statuses@npm:^2.0.0": version: 2.0.1 resolution: "statuses@npm:2.0.1" checksum: 18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb @@ -37476,12 +37829,12 @@ __metadata: languageName: node linkType: hard -"strict-event-emitter@npm:^0.2.4": - version: 0.2.7 - resolution: "strict-event-emitter@npm:0.2.7" +"strict-event-emitter@npm:^0.2.4, strict-event-emitter@npm:^0.2.6": + version: 0.2.8 + resolution: "strict-event-emitter@npm:0.2.8" dependencies: events: ^3.3.0 - checksum: 111691e7d3fce0810586ccd8e8234af883ad3b121ef69091c7e260c32299d1ba085a95238ad09b43478bc5e9e80370f2fcb8114716e343be6f44bfc08fab4142 + checksum: 6ac06fe72a6ee6ae64d20f1dd42838ea67342f1b5f32b03b3050d73ee6ecee44b4d5c4ed2965a7154b47991e215f373d4e789e2b2be2769cd80e356126c2ca53 languageName: node linkType: hard @@ -37870,32 +38223,31 @@ __metadata: languageName: node linkType: hard -"superagent@npm:^8.0.0": - version: 8.0.0 - resolution: "superagent@npm:8.0.0" +"superagent@npm:^8.0.5": + version: 8.0.9 + resolution: "superagent@npm:8.0.9" dependencies: component-emitter: ^1.3.0 - cookiejar: ^2.1.3 + cookiejar: ^2.1.4 debug: ^4.3.4 fast-safe-stringify: ^2.1.1 form-data: ^4.0.0 - formidable: ^2.0.1 + formidable: ^2.1.2 methods: ^1.1.2 mime: 2.6.0 - qs: ^6.10.3 - readable-stream: ^3.6.0 - semver: ^7.3.7 - checksum: 14343e59327eafd85fa230acb876017079d5efcecc72a56566abc0f965220bb460af2e070dddecd9e2856410b2d2b318d81d9cc1d342aa5922da93c29a295dd7 + qs: ^6.11.0 + semver: ^7.3.8 + checksum: 5d00cdc7ceb5570663da80604965750e6b1b8d7d7442b7791e285c62bcd8d578a8ead0242a2426432b59a255fb42eb3a196d636157538a1392e7b6c5f1624810 languageName: node linkType: hard -"supertest@npm:^6.1.3, supertest@npm:^6.1.6, supertest@npm:^6.2.4": - version: 6.2.4 - resolution: "supertest@npm:6.2.4" +"supertest@npm:^6.1.3, supertest@npm:^6.1.6, supertest@npm:^6.2.4, supertest@npm:^6.3.3": + version: 6.3.3 + resolution: "supertest@npm:6.3.3" dependencies: methods: ^1.1.2 - superagent: ^8.0.0 - checksum: f2ddc4f3ba467a5c4036dd4aad41351e4b60eb13c39ecf5233ccd2ebb425504073b2b7036c973a70c7047f5c6bc1b9fef096b7bbff114d357cbe80654441db23 + superagent: ^8.0.5 + checksum: 38239e517f7ba62b7a139a79c5c48d55f8d67b5ff4b6e51d5b07732ca8bbc4a28ffa1b10916fbb403dd013a054dbf028edc5850057d9a43aecbff439d494673e languageName: node linkType: hard @@ -38137,16 +38489,16 @@ __metadata: linkType: hard "tar@npm:^6.0.2, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2": - version: 6.1.13 - resolution: "tar@npm:6.1.13" + version: 6.1.14 + resolution: "tar@npm:6.1.14" dependencies: chownr: ^2.0.0 fs-minipass: ^2.0.0 - minipass: ^4.0.0 + minipass: ^5.0.0 minizlib: ^2.1.1 mkdirp: ^1.0.3 yallist: ^4.0.0 - checksum: 8a278bed123aa9f53549b256a36b719e317c8b96fe86a63406f3c62887f78267cea9b22dc6f7007009738509800d4a4dccc444abd71d762287c90f35b002eb1c + checksum: a1be0815a9bdc97dfca7c6c2d71d1b836f8ba9314684e2c412832f0f59cc226d4c13da303d6bc30925e82f634cc793f40da79ae72f3e96fb87c23d0f4efd5207 languageName: node linkType: hard @@ -38240,14 +38592,14 @@ __metadata: linkType: hard "terser-webpack-plugin@npm:*, terser-webpack-plugin@npm:^5.1.3": - version: 5.3.7 - resolution: "terser-webpack-plugin@npm:5.3.7" + version: 5.3.8 + resolution: "terser-webpack-plugin@npm:5.3.8" dependencies: "@jridgewell/trace-mapping": ^0.3.17 jest-worker: ^27.4.5 schema-utils: ^3.1.1 serialize-javascript: ^6.0.1 - terser: ^5.16.5 + terser: ^5.16.8 peerDependencies: webpack: ^5.1.0 peerDependenciesMeta: @@ -38257,13 +38609,13 @@ __metadata: optional: true uglify-js: optional: true - checksum: 095e699fdeeb553cdf2c6f75f983949271b396d9c201d7ae9fc633c45c1c1ad14c7257ef9d51ccc62213dd3e97f875870ba31550f6d4f1b6674f2615562da7f7 + checksum: 0ffc2a1949b1fd60ef9c815c4629b9817656db612bb58c5db96e3b04204c86afd142b115392e48733364edc7bf95131f54c10174c05f046ba8f2adead6b06c3c languageName: node linkType: hard -"terser@npm:^5.10.0, terser@npm:^5.16.5": - version: 5.16.6 - resolution: "terser@npm:5.16.6" +"terser@npm:^5.10.0, terser@npm:^5.16.8": + version: 5.17.1 + resolution: "terser@npm:5.17.1" dependencies: "@jridgewell/source-map": ^0.3.2 acorn: ^8.5.0 @@ -38271,7 +38623,7 @@ __metadata: source-map-support: ~0.5.20 bin: terser: bin/terser - checksum: f763a7bcc7b98cb2bfc41434f7b92bfe8a701a12c92ea6049377736c8e6de328240d654a20dfe15ce170fd783491b9873fad9f4cd8fee4f6c6fb8ca407859dee + checksum: 69b0e80e3c4084db2819de4d6ae8a2ba79f2fcd7ed6df40fe4b602ec7bfd8e889cc63c7d5268f30990ffecbf6eeda18f857adad9386fe2c2331b398d58ed855c languageName: node linkType: hard @@ -39046,7 +39398,7 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^0.7.18, ua-parser-js@npm:^0.7.30": +"ua-parser-js@npm:^0.7.30": version: 0.7.33 resolution: "ua-parser-js@npm:0.7.33" checksum: 1510e9ec26fcaf0d8c6ae8f1078a8230e8816f083e1b5f453ea19d06b8ef2b8a596601c92148fd41899e8b3e5f83fa69c42332bd5729b931a721040339831696 @@ -39446,6 +39798,15 @@ __metadata: languageName: node linkType: hard +"use-composed-ref@npm:^1.3.0": + version: 1.3.0 + resolution: "use-composed-ref@npm:1.3.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: f771cbadfdc91e03b7ab9eb32d0fc0cc647755711801bf507e891ad38c4bbc5f02b2509acadf9c965ec9c5f2f642fd33bdfdfb17b0873c4ad0a9b1f5e5e724bf + languageName: node + linkType: hard + "use-deep-compare-effect@npm:^1.8.1": version: 1.8.1 resolution: "use-deep-compare-effect@npm:1.8.1" @@ -39468,6 +39829,32 @@ __metadata: languageName: node linkType: hard +"use-isomorphic-layout-effect@npm:^1.1.1": + version: 1.1.2 + resolution: "use-isomorphic-layout-effect@npm:1.1.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: a6532f7fc9ae222c3725ff0308aaf1f1ddbd3c00d685ef9eee6714fd0684de5cb9741b432fbf51e61a784e2955424864f7ea9f99734a02f237b17ad3e18ea5cb + languageName: node + linkType: hard + +"use-latest@npm:^1.2.1": + version: 1.2.1 + resolution: "use-latest@npm:1.2.1" + dependencies: + use-isomorphic-layout-effect: ^1.1.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: ed3f2ddddf6f21825e2ede4c2e0f0db8dcce5129802b69d1f0575fc1b42380436e8c76a6cd885d4e9aa8e292e60fb8b959c955f33c6a9123b83814a1a1875367 + languageName: node + linkType: hard + "use-memo-one@npm:^1.1.1": version: 1.1.1 resolution: "use-memo-one@npm:1.1.1" @@ -39817,15 +40204,15 @@ __metadata: languageName: node linkType: hard -"vm2@npm:^3.9.17": - version: 3.9.17 - resolution: "vm2@npm:3.9.17" +"vm2@npm:^3.9.18": + version: 3.9.19 + resolution: "vm2@npm:3.9.19" dependencies: acorn: ^8.7.0 acorn-walk: ^8.2.0 bin: vm2: bin/vm2 - checksum: 9a03740a40ab2be5e3348a95fb31512da1a3c85318febb07e5299fa103ff05bcd7b6f458211fa38a1281dc27beccd04ff90355fc1d34fe2ee6ca10d0bb8c6f35 + checksum: fc6cf553134145cd7bb5246985bf242b056e3fb5ea71e2eef6710b2a5d6c6119cc6bc960435ff62480ee82efb43369be8f4db07b6690916ae7d3b2e714f395d8 languageName: node linkType: hard