Merge branch 'master' of github.com:splunk/backstage into feature/update-splunk-on-call-invalid-team

This commit is contained in:
Heather Lee
2021-05-25 15:39:41 -07:00
732 changed files with 30177 additions and 3250 deletions
@@ -1,5 +0,0 @@
---
'@backstage/plugin-cost-insights': minor
---
make change ratio optional
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
fix for query parameters with null groups
-10
View File
@@ -1,10 +0,0 @@
---
'@backstage/backend-common': patch
'@backstage/create-app': patch
'@backstage/integration': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-github-actions': patch
'@backstage/plugin-scaffolder-backend': patch
---
The default `@octokit/rest` dependency was bumped to `"^18.5.3"`.
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/cli': patch
'@backstage/core': patch
---
Fix some transitive dependency warnings in yarn
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-react': patch
---
Fetch relations in batches in `useRelatedEntities`
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/core': patch
'@backstage/plugin-catalog-import': patch
---
Bump react-hook-form version to be the same for the entire project.
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/integration': patch
'@backstage/plugin-catalog-backend': minor
---
Updates the `GithubCredentialsProvider` to return the token type, it can either be `token` or `app` depending on the authentication method.
Update the `GithubOrgReaderProcessor` NOT to query for email addresses if GitHub Apps is used for authentication, this is due to inconsistencies in the GitHub API when using server to server communications and installation tokens. https://github.community/t/api-v4-unable-to-retrieve-email-resource-not-accessible-by-integration/13831/4 for more info.
**Removes** deprecated GithubOrgReaderProcessor provider configuration(`catalog.processors.githubOrg`). If you're using the deprecated config section make sure to migrate to [integrations](https://backstage.io/docs/integrations/github/locations) instead.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/catalog-client': patch
'@backstage/catalog-model': patch
'@backstage/plugin-catalog-backend': patch
---
Foundation for standard entity status values
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
Expose `BitbucketRepositoryParser` introduced in [#5295](https://github.com/backstage/backstage/pull/5295)
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/cli': patch
---
Added lax option to backstage-cli app:build command
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Fix GithubPR built-in action `credentialsProvider.getCredentials` URL.
Adding Documentation for GitHub PR built-in action.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-api': patch
---
Add a global type definition for `Symbol.observable`, fix type checking in projects that didn't already have it defined.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Add support for transforming yaml files in jest with 'yaml-jest'
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-techdocs': patch
---
Set admonition font size to 1rem in TechDocs to align with the rest of the document's font sizes.
Fixes #5448 and #5541.
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/plugin-kubernetes': patch
'@backstage/plugin-kubernetes-backend': patch
---
Adds @backstage/plugin-kubernetes-common library to share types between kubernetes frontend and backend.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
bump `jsonschema` from 1.2.7 to 1.4.0
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/create-app': patch
---
Added newer entity relationship cards to the default `@backstage/create-app` template:
- `EntityDependsOnComponentsCard`
- `EntityDependsOnResourcesCard`
- `EntityHasResourcesCard`
- `EntityHasSubcomponentsCard`
The `EntityLinksCard` was also added to the overview page. To apply these to your Backstage application, compare against the updated [EntityPage.tsx](https://github.com/backstage/backstage/blob/371760ca2493c8f63e9b44ecc57cc8488131ba5b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx)
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-tech-radar': patch
---
Update README for composability
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
Add support for non-organization accounts in GitHub Discovery
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder': patch
---
Close eventSource upon completion of a scaffolder task
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Fixes #5529, a bug that prevented TechDocs from rendering pages containing malformed links.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Make git config optional for techdocs feedback links
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-proxy-backend': patch
---
Prefix proxy routes with `/` if not present in configuration
-9
View File
@@ -1,9 +0,0 @@
---
'@backstage/catalog-model': patch
'@backstage/cli': patch
'@backstage/config-loader': patch
'@backstage/config': patch
'@backstage/plugin-scaffolder': patch
---
Bump `json-schema` dependency from `0.2.5` to `0.3.0`.
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/techdocs-common': patch
---
Adding optional config to enable S3-like API for tech-docs using s3ForcePathStyle option.
This allows providers like LocalStack, Minio and Wasabi (+possibly others) to be used to host tech docs.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/catalog-client': patch
---
Allow `filter` parameter to be specified multiple times
+1
View File
@@ -16,6 +16,7 @@
/plugins/search-* @backstage/techdocs-core
/plugins/techdocs @backstage/techdocs-core
/plugins/techdocs-backend @backstage/techdocs-core
/plugins/ilert @yacut
/packages/search-common @backstage/techdocs-core
/packages/techdocs-common @backstage/techdocs-core
/.changeset/cost-insights-* @backstage/silver-lining
+1
View File
@@ -9,6 +9,7 @@ exemptLabels:
- plugin
- help wanted
- good first issue
- rfc
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
+9
View File
@@ -1,5 +1,6 @@
Apdex
Api
automations
Autoscaling
Avro
Bigtable
@@ -32,12 +33,16 @@ GitLab
Grafana
GraphQL
Hackathons
Helidon
Heroku
hoc
Hostname
Iain
JaCoCo
JavaScript
Jira
Kaewkasi
kubernetes
Knex
Leasot
Lerna
@@ -47,6 +52,8 @@ Minikube
Minio
Mkdocs
Monorepo
microservices
microservice
Namespaces
OAuth
Okta
@@ -167,6 +174,7 @@ mailto
maintainership
makefile
md
memcache
microsite
middleware
minikube
@@ -177,6 +185,7 @@ mkdocs
monorepo
monorepos
msw
mysql
namespace
namespaced
namespaces
+1
View File
@@ -20,3 +20,4 @@ jobs:
version: yarn release
env:
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
HUSKY: '0'
+1
View File
@@ -0,0 +1 @@
_
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged
+31 -27
View File
@@ -1,27 +1,31 @@
| Organization | Contact | Description of Use |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. |
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling |
| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling |
| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks |
| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. |
| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. |
| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. |
| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit |
| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go |
| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling |
| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. |
| [Trendyol](https://trendyol.com) | [Erdogan Oksuz](https://github.com/erdoganoksuz) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. |
| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. |
| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. |
| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a teams engineering dependencies. |
| Organization | Contact | Description of Use |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. |
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling |
| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling |
| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks |
| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. |
| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. |
| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. |
| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit |
| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go |
| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling |
| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. |
| [Trendyol](https://trendyol.com) | [Erdogan Oksuz](https://github.com/erdoganoksuz) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. |
| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. |
| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. |
| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a teams engineering dependencies. |
| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. |
| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. |
| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. |
| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. |
+1 -1
View File
@@ -93,7 +93,7 @@ Which will look something like the following in the repo;
```
Awesome commit message
Signed-off-by: Jane Smith jane.smith@example.com
Signed-off-by: Jane Smith <jane.smith@example.com>
```
- In case you forgot to add it to the most recent commit, use `git commit --amend --signoff`
+1 -1
View File
@@ -27,5 +27,5 @@ People that have made significant contributions to the project and earned write
- Andrew Thauer - Wealthsimple (GitHub: [andrewthauer](https://github.com/andrewthauer))
- Oliver Sand - SDA SE (GitHub: [Fox32](https://github.com/Fox32))
- David Tuite - Roadie (GitHub: [dtuite](https://github.com/dtuite))
- Adam Harvey - DXC Technology (GitHub: [adamdmharvey](https://github.com/adamdmharvey))
- Adam Harvey - Cisco (GitHub: [adamdmharvey](https://github.com/adamdmharvey))
- Dominik Henneke - SDA SE (GitHub: [dhenneke](https://github.com/dhenneke))
+7
View File
@@ -81,6 +81,13 @@ proxy:
headers:
Authorization: ${SENTRY_TOKEN}
'/ilert':
target: https://api.ilert.com
allowedMethods: ['GET', 'POST', 'PUT']
allowedHeaders: ['Authorization']
headers:
Authorization: ${ILERT_AUTH_HEADER}
organization:
name: My Company
+1 -1
View File
@@ -5,7 +5,7 @@
"license": "MIT",
"private": true,
"dependencies": {
"cypress": "^6.4.0",
"cypress": "^7.3.0",
"typescript": "^4.1.3"
}
}
+89 -82
View File
@@ -53,7 +53,12 @@
dependencies:
any-observable "^0.3.0"
"@types/sinonjs__fake-timers@^6.0.1":
"@types/node@^14.14.31":
version "14.17.0"
resolved "https://registry.npmjs.org/@types/node/-/node-14.17.0.tgz#3ba770047723b3eeb8dc9fca02cce8a7fb6378da"
integrity sha512-w8VZUN/f7SSbvVReb9SWp6cJFevxb4/nkG65yLAya//98WgocKm5PLDAtSs5CtJJJM+kHmJjO/6mmYW4MHShZA==
"@types/sinonjs__fake-timers@^6.0.2":
version "6.0.2"
resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae"
integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==
@@ -117,7 +122,7 @@ any-observable@^0.3.0:
resolved "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b"
integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==
arch@^2.1.2:
arch@^2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
@@ -171,7 +176,7 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
blob-util@2.0.2:
blob-util@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
@@ -242,10 +247,10 @@ check-more-types@^2.24.0:
resolved "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
ci-info@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.1.1.tgz#9a32fcefdf7bcdb6f0a7e1c0f8098ec57897b80a"
integrity sha512-kdRWLBIJwdsYJWYJFtAFFYxybguqeF91qpZaggjG5Nf8QKdizFG2hjqvaTXbxFIcYbSaD74KpAXv6BSm17DHEQ==
cli-cursor@^1.0.2:
version "1.0.2"
@@ -359,18 +364,19 @@ cross-spawn@^7.0.0:
shebang-command "^2.0.0"
which "^2.0.1"
cypress@^6.4.0:
version "6.4.0"
resolved "https://registry.npmjs.org/cypress/-/cypress-6.4.0.tgz#432c516bf4f1a0f042a6aa1f2c3a4278fa35a8b2"
integrity sha512-SrsPsZ4IBterudkoFYBvkQmXOVxclh1/+ytbzpV8AH/D2FA+s2Qy5ISsaRzOFsbQa4KZWoi3AKwREmF1HucYkg==
cypress@^7.3.0:
version "7.3.0"
resolved "https://registry.npmjs.org/cypress/-/cypress-7.3.0.tgz#17345b8d18681c120f033e7d8fd0f0271e9d0d51"
integrity sha512-aseRCH1tRVCrM6oEfja6fR/bo5l6e4SkHRRSATh27UeN4f/ANC8U7tGIulmrISJVy9xuOkOdbYKbUb2MNM+nrw==
dependencies:
"@cypress/listr-verbose-renderer" "^0.4.1"
"@cypress/request" "^2.88.5"
"@cypress/xvfb" "^1.2.4"
"@types/sinonjs__fake-timers" "^6.0.1"
"@types/node" "^14.14.31"
"@types/sinonjs__fake-timers" "^6.0.2"
"@types/sizzle" "^2.3.2"
arch "^2.1.2"
blob-util "2.0.2"
arch "^2.2.0"
blob-util "^2.0.2"
bluebird "^3.7.2"
cachedir "^2.3.0"
chalk "^4.1.0"
@@ -378,27 +384,26 @@ cypress@^6.4.0:
cli-table3 "~0.6.0"
commander "^5.1.0"
common-tags "^1.8.0"
dayjs "^1.9.3"
debug "^4.1.1"
eventemitter2 "^6.4.2"
execa "^4.0.2"
dayjs "^1.10.4"
debug "4.3.2"
eventemitter2 "^6.4.3"
execa "4.1.0"
executable "^4.1.1"
extract-zip "^1.7.0"
fs-extra "^9.0.1"
fs-extra "^9.1.0"
getos "^3.2.1"
is-ci "^2.0.0"
is-installed-globally "^0.3.2"
is-ci "^3.0.0"
is-installed-globally "~0.4.0"
lazy-ass "^1.6.0"
listr "^0.14.3"
lodash "^4.17.19"
lodash "^4.17.21"
log-symbols "^4.0.0"
minimist "^1.2.5"
moment "^2.29.1"
ospath "^1.2.2"
pretty-bytes "^5.4.1"
ramda "~0.26.1"
pretty-bytes "^5.6.0"
ramda "~0.27.1"
request-progress "^3.0.0"
supports-color "^7.2.0"
supports-color "^8.1.1"
tmp "~0.2.1"
untildify "^4.0.0"
url "^0.11.0"
@@ -416,11 +421,18 @@ date-fns@^1.27.2:
resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
dayjs@^1.9.3:
dayjs@^1.10.4:
version "1.10.4"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==
debug@4.3.2:
version "4.3.2"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
dependencies:
ms "2.1.2"
debug@^2.6.9:
version "2.6.9"
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -435,13 +447,6 @@ debug@^3.1.0:
dependencies:
ms "^2.1.1"
debug@^4.1.1:
version "4.3.1"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
dependencies:
ms "2.1.2"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -477,12 +482,12 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
eventemitter2@^6.4.2:
version "6.4.3"
resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820"
integrity sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ==
eventemitter2@^6.4.3:
version "6.4.4"
resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b"
integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==
execa@^4.0.2:
execa@4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
@@ -580,7 +585,7 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
fs-extra@^9.0.1:
fs-extra@^9.1.0:
version "9.1.0"
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
@@ -628,12 +633,12 @@ glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
global-dirs@^2.0.1:
version "2.1.0"
resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d"
integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==
global-dirs@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686"
integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==
dependencies:
ini "1.3.7"
ini "2.0.0"
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.5"
@@ -702,17 +707,17 @@ inherits@2, inherits@^2.0.3, inherits@~2.0.3:
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@1.3.7:
version "1.3.7"
resolved "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
is-ci@^2.0.0:
ini@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
is-ci@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994"
integrity sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==
dependencies:
ci-info "^2.0.0"
ci-info "^3.1.1"
is-fullwidth-code-point@^1.0.0:
version "1.0.0"
@@ -731,13 +736,13 @@ is-fullwidth-code-point@^3.0.0:
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-installed-globally@^0.3.2:
version "0.3.2"
resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141"
integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==
is-installed-globally@~0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==
dependencies:
global-dirs "^2.0.1"
is-path-inside "^3.0.1"
global-dirs "^3.0.0"
is-path-inside "^3.0.2"
is-observable@^1.1.0:
version "1.1.0"
@@ -746,10 +751,10 @@ is-observable@^1.1.0:
dependencies:
symbol-observable "^1.1.0"
is-path-inside@^3.0.1:
version "3.0.2"
resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017"
integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==
is-path-inside@^3.0.2:
version "3.0.3"
resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-promise@^2.1.0:
version "2.2.2"
@@ -879,10 +884,10 @@ lodash.once@^4.1.1:
resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash@^4.17.19:
version "4.17.20"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@^1.0.2:
version "1.0.2"
@@ -953,11 +958,6 @@ mkdirp@^0.5.4:
dependencies:
minimist "^1.2.5"
moment@^2.29.1:
version "2.29.1"
resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -1056,10 +1056,10 @@ pify@^2.2.0:
resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
pretty-bytes@^5.4.1:
version "5.5.0"
resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e"
integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA==
pretty-bytes@^5.6.0:
version "5.6.0"
resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
process-nextick-args@~2.0.0:
version "2.0.1"
@@ -1099,10 +1099,10 @@ querystring@0.2.0:
resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
ramda@~0.26.1:
version "0.26.1"
resolved "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
ramda@~0.27.1:
version "0.27.1"
resolved "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==
readable-stream@^2.2.2:
version "2.3.7"
@@ -1277,13 +1277,20 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"
supports-color@^7.1.0, supports-color@^7.2.0:
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
supports-color@^8.1.1:
version "8.1.1"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
dependencies:
has-flag "^4.0.0"
symbol-observable@^1.1.0:
version "1.2.0"
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
@@ -125,8 +125,8 @@ entity, and for machines and other components to reference the entity (e.g. in
URLs or from other entity specification files).
Names must be unique per kind, within a given namespace (if specified), at any
point in time. Names may be reused at a later time, after an entity is deleted
from the registry.
point in time. This uniqueness constraint is also case insensitive. Names may be
reused at a later time, after an entity is deleted from the registry.
Names are required to follow a certain format. Entities that do not follow those
rules will not be accepted for registration in the catalog. The ruleset is
@@ -139,18 +139,6 @@ follows.
Example: `visits-tracking-service`, `CircleciBuildsDs_avro_gcs`
In addition to this, names are passed through a normalization function and then
compared to the same normalized form of other entity names and made sure to not
collide. This rule of uniqueness exists to avoid situations where e.g. both
`my-component` and `MyComponent` are registered side by side, which leads to
confusion and risk. The normalization function is also configurable, but the
default behavior is as follows.
- Strip out all characters outside of the set `[a-zA-Z0-9]`
- Convert to lowercase
Example: `CircleciBuildsDs_avro_gcs` -> `circlecibuildsdsavrogcs`
### `namespace`
The `name` of a namespace that the entity belongs to. This field is optional,
+2 -2
View File
@@ -31,8 +31,8 @@ auth:
providers:
gitlab:
development:
clientId: ${AUTH_GITLAB_APPLICATION_ID}
clientSecret: ${AUTH_GITLAB_SECRET}
clientId: ${AUTH_GITLAB_CLIENT_ID}
clientSecret: ${AUTH_GITLAB_CLIENT_SECRET}
## uncomment if using self-hosted GitLab
# audience: https://gitlab.company.com
```
-2
View File
@@ -77,7 +77,6 @@ built-in providers:
+
const app = createApp({
apis,
plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => (
+ <SignInPage
@@ -96,7 +95,6 @@ To also allow unauthenticated guest access, use the `providers` prop for
```diff
const app = createApp({
apis,
plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => (
+ <SignInPage
@@ -33,6 +33,7 @@ kubernetes:
- type: 'gke'
projectId: 'gke-clusters'
region: 'europe-west1'
skipTLSVerify: true
```
### `serviceLocatorMethod`
@@ -129,6 +130,11 @@ The Google Cloud project to look for Kubernetes clusters in.
The Google Cloud region to look for Kubernetes clusters in. Defaults to all
regions.
##### `skipTLSVerify`
This determines whether or not the Kubernetes client verifies the TLS
certificate presented by the API server. Defaults to `false`.
### `customResources` (optional)
Configures which [custom resources][3] to look for when returning an entity's
@@ -23,6 +23,7 @@ we recommend that you name them `catalog-info.yaml`.
- [Common to All Kinds: The Envelope](#common-to-all-kinds-the-envelope)
- [Common to All Kinds: The Metadata](#common-to-all-kinds-the-metadata)
- [Common to All Kinds: Relations](#common-to-all-kinds-relations)
- [Common to All Kinds: Status](#common-to-all-kinds-status)
- [Kind: Component](#kind-component)
- [Kind: Template](#kind-template)
- [Kind: API](#kind-api)
@@ -396,6 +397,68 @@ with it (such as the default kind being `Group` if not specified).
See the [well-known relations section](well-known-relations.md) for a list of
well-known / common relations and their semantics.
## Common to All Kinds: Status
The `status` root object is a read-only set of statuses, pertaining to the
current state or health of the entity, described in the
[well-known statuses section](well-known-statuses.md).
Currently, the only defined field is the `items` array. Each of its items
contains a specific data structure that describes some aspect of the state of
the entity, as seen from the point of view of some specific system. Different
systems may contribute to this array, under their own respective `type` keys.
The current main use case for this field is for the ingestion processes of the
catalog itself to convey information about errors and warnings back to the user.
A status field as part of a single entity that's read out of the API may look as
follows.
```js
{
// ...
"status": {
"items": [
{
"type": "backstage.io/catalog-processing",
"level": "error",
"message": "NotFoundError: File not found",
"error": {
"name": "NotFoundError",
"message": "File not found",
"stack": "..."
}
}
]
},
"spec": {
// ...
}
}
```
The fields of a status item are:
| Field | Type | Description |
| --------- | ------ | ------------------------------------------------------------------------------------------------ |
| `type` | String | The type of status as a unique key per source. Each type may appear more than once in the array. |
| `level` | String | The level / severity of the status item: 'info', 'warning, or 'error'. |
| `message` | String | A brief message describing the status, intended for human consumption. |
| `error` | Object | An optional serialized error object related to the status. |
The `type` is an arbitrary string, but we recommend that types that are not
strictly private within the organization be namespaced to avoid collisions.
Types emitted by Backstage core processes will for example be prefixed with
`backstage.io/` as in the example above.
Entity descriptor YAML files are not supposed to contain a `status` root key.
Instead, catalog processors analyze the entity descriptor data and its
surroundings, and deduce status entries that are then attached onto the entity
as read from the catalog.
See the [well-known statuses section](well-known-statuses.md) for a list of
well-known / common status types.
## Kind: Component
Describes the following entity kind:
@@ -657,6 +720,25 @@ You can find out more about the `parameters` key
You can find out more about the `steps` key
[here](../software-templates/writing-templates.md)
### `spec.owner` [optional]
An [entity reference](#string-references) to the owner of the component, e.g.
`artist-relations-team`. This field is required.
In Backstage, the owner of a Template is the singular entity (commonly a team)
that bears ultimate responsibility for the Template, and has the authority and
capability to develop and maintain it. They will be the point of contact if
something goes wrong, or if features are to be requested. The main purpose of
this field is for display purposes in Backstage, so that people looking at
catalog items can get an understanding of to whom this Template belongs. It is
not to be used by automated processes to for example assign authorization in
runtime systems. There may be others that also develop or otherwise touch the
Template, but there will always be one ultimate owner.
| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type |
| ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- |
| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) |
## Kind: API
Describes the following entity kind:
@@ -1,6 +1,7 @@
---
id: extending-the-model
title: Extending the model
# prettier-ignore
description: Documentation on extending the catalog model
---
@@ -301,9 +302,9 @@ Example intents:
> "We have this concept of service maintainership, separate from ownership, that
> we would like to make relations to individual users for."
> We feel that we want to explicitly model the team-to-global-department mapping
> as a relation, because it is core to our org setup and we frequently query for
> it.
> "We feel that we want to explicitly model the team-to-global-department
> mapping as a relation, because it is core to our org setup and we frequently
> query for it."
Any processor can emit relations for entities as they are being processed, and
new processors can be added when building the backend catalog using the
@@ -349,3 +350,46 @@ If you want to extend the use of an established relation type in a way that has
an effect outside of your organization, reach out to the Backstage maintainers
or a support partner to discuss risk/impact. It may even be that one end of the
relation could be considered for addition to the core.
## Adding a New Status field
Example intent:
> "We would like to convey entity statuses through the catalog in a generic way,
> as an integration layer. Our monitoring and alerting system has a plugin with
> Backstage, and it would be useful if the entity's status field contained the
> current alert state close to the actual entity data for anyone to consume. We
> find the `status.items` semantics a poor fit, so we would prefer to make our
> own custom field under `status` for these purposes."
We have not yet ventured to define any generic semantics for the `status`
object. We recommend sticking with the `status.items` mechanism where possible
(see below), since third party consumers will not be able to consume your status
information otherwise. Please reach out to the maintainers on Discord or by
making a GitHub issue describing your use case if you are interested in this
topic.
## Adding a New Status Item Type
Example intent:
> "The semantics of the entity `status.items` field are fine for our needs, but
> we want to contribute our own type of status into that array instead of the
> catalog specific one."
This is a simple, low risk way of adding your own status information to
entities. Consumers will be able to easily track and display the status together
with other types / sources.
We recommend that any status type that are not strictly private within the
organization be namespaced to avoid collisions. Statuses emitted by Backstage
core processes will for example be prefixed with `backstage.io/`, your
organization may prefix with `my-org.net/`, and `pagerduty.com/active-alerts`
could be a sensible complete status item type for that particular external
system.
The mechanics for how to emit custom statuses is not in place yet, so if this is
of interest to you, you might consider contacting the maintainers on Discord or
my making a GitHub issue describing your use case.
[This issue](https://github.com/backstage/backstage/issues/2292) also contains
more context.
@@ -0,0 +1,55 @@
---
id: well-known-statuses
title: Well-known Status fields of Catalog Entities
sidebar_label: Well-known Statuses
# prettier-ignore
description: Lists a number of well known entity statuses, that have defined semantics. They can be attached to catalog entities and consumed by plugins as needed.
---
This section lists well known
[entity statuses](descriptor-format.md#common-to-all-kinds-status), that have
defined semantics. They can be attached to catalog entities and consumed by
plugins as needed.
If you are looking to extend the statuses, see
[Extending the model](extending-the-model.md).
## Common Fields
The `status` object of an entity is currently left unrestricted, except for the
`items` field. Its structure is defined in the
[descriptor format](descriptor-format.md#common-to-all-kinds-status) section.
We reserve the right to extend this model in the future. This status is in
active development and its format will change unexpectedly. Do not consume it in
your own code until such a time that this documentation has been updated.
## Status Item Types
This is a (non-exhaustive) list of `status.items.[].type` values that are known
to be in active use.
### `backstage.io/catalog-processing`
Expresses an aspect of the current status of the catalog's ingestion of this
entity. Errors that may appear here include inability to read from the remote
SCM provider, syntax errors in the YAML file, and similar.
Note that the entity data itself may be of an older version, when errors are
present. The ingestion system keeps the old valid entity data untouched when
possible, so the errors described in this state may not seem to align with the
rest of the entity, because they pertain to a remote that could not be
successfully ingested. This is normal.
```yaml
# Example:
status:
items:
- type: backstage.io/catalog-processing
level: error
message: 'NotFoundError: File not found'
error:
name: NotFoundError
message: File not found
stack: ...
```
@@ -70,6 +70,10 @@ creating a file called `packages/backend/src/plugins/scaffolder.ts` with the
following contents to get you up and running quickly.
```ts
import {
DockerContainerRunner,
SingleHostDiscovery,
} from '@backstage/backend-common';
import {
CookieCutter,
createRouter,
@@ -78,7 +82,6 @@ import {
CreateReactAppTemplater,
Templaters,
} from '@backstage/plugin-scaffolder-backend';
import { SingleHostDiscovery } from '@backstage/backend-common';
import type { PluginEnvironment } from '../types';
import Docker from 'dockerode';
import { CatalogClient } from '@backstage/catalog-client';
@@ -89,8 +92,11 @@ export default async function createPlugin({
database,
reader,
}: PluginEnvironment) {
const cookiecutterTemplater = new CookieCutter();
const craTemplater = new CreateReactAppTemplater();
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
const cookiecutterTemplater = new CookieCutter({ containerRunner });
const craTemplater = new CreateReactAppTemplater({ containerRunner });
const templaters = new Templaters();
templaters.register('cookiecutter', cookiecutterTemplater);
@@ -99,8 +105,6 @@ export default async function createPlugin({
const preparers = await Preparers.fromConfig(config, { logger });
const publishers = await Publishers.fromConfig(config, { logger });
const dockerClient = new Docker();
const discovery = SingleHostDiscovery.fromConfig(config);
const catalogClient = new CatalogClient({ discoveryApi: discovery });
@@ -110,7 +114,6 @@ export default async function createPlugin({
publishers,
logger,
config,
dockerClient,
database,
catalogClient,
reader,
@@ -0,0 +1,334 @@
---
id: migrating-from-v1alpha1-to-v1beta2
title: Migrating to v1beta2 templates
# prettier-ignore
description:
How to move your old templates from v1alpha1 to the more declarative v1beta2
---
# What's new?
Previously, the scaffolder was very restricted in what you could do when
creating new software components from templates. There were three scaffolding
steps which was pretty hard to extend and add new functionality to, difficult to
re-use logic between templates. There used to be a fixed pipeline of
`preparers`, `templaters`, and `publishers`, which were defined by the backend
and needed to be run for each template. This is now changed, to give the
template total control over what should be executed as part of the templating
run. This makes templates a little more declarative as you can now register
different `actions` or `functions` with the `scaffolder-backend` which you then
can decide how, and in what order, to run using the template definition YAML
file.
We've also made some improvements, and added some helpers to work with
cookiecutter. The skeleton for a template can now be stored in a different place
to where your entity definition is: previously you needed to have your
`template.yaml` next to the skeleton source (`{{cookiecutter.component_id}}`
directory), but now that's not the case. Part of the changes with the `v1beta2`
syntax is that you can grab your template source from any repository, and re-use
them between templates.
We've also renamed the `schema` property to `parameters` as this makes more
sense when using them as parameters to the actions or steps that you've setup
for your templates. There's the added benefit that you can now assign an array
to the `parameters` property, which will then give you multiple steps in the UI,
so you can split apart your input parameters and group them as needed rather
than having one long list of input fields.
## The `parameters` property
The `schema` key has now been renamed to `parameters` with a few more features.
You can pass an array now to break apart the input form into different steps in
the UI. You can also specify `ui:schema` fields that are passed along to
[`react-jsonschema-form`](https://rjsf-team.github.io/react-jsonschema-form/)
inline with the JSON schema.
```yaml
spec:
parameters:
- title: Fill in some steps
required:
- name
properties:
name:
title: Name
type: string
description: Unique name of the component
ui:autofocus: true
ui:options:
rows: 5
```
## The `steps` property
`v1beta2` template syntax introduces the new `steps` property, which is an array
of `actions` that the scaffolder will run in combination with the user input
that is declared in the `schema`. Actions look like the following:
```yaml
spec:
steps:
- id: publish # a unique id for the step, can be anything you like
name: Publish # a user friendly name for the step, this is what is shown in the frontend
action: publish:github # the action ID that has been registered with the scaffolder-backend
input: # parameters that are passed as input to the action handler function
allowedHosts: ['github.com']
description: 'This is {{ parameters.name }}' # handlebars templating is supported with the values from the parameters section in the same file.
repoUrl: '{{ parameters.repoUrl }}'
```
# Migrating a `v1alpha1` template
## The template definition (.yaml)
### `parameters`
Because of the changes to invert the control to the `template.yaml` definition
for running the workflow, we need to adjust the `schema` property and we also
now need to define what the template is actually going to do as part of the
template run.
A simple migration would move the following yaml:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Template
metadata:
name: react-ssr-template
title: React SSR Template
description: Create a website powered with Next.js
tags:
- recommended
- react
spec:
owner: web@example.com
templater: cookiecutter
type: website
path: '.'
schema:
required:
- component_id
- description
properties:
component_id:
title: Name
type: string
description: Unique name of the component
description:
title: Description
type: string
description: Help others understand what this website is for.
```
To something that looks like the following:
```yaml
apiVersion: backstage.io/v1beta2
kind: Template
metadata:
name: react-ssr-template
title: React SSR Template
description: Create a website powered with Next.js
tags:
- recommended
- react
spec:
owner: web@example.com
type: website
parameters:
- title: Add some input
required:
- component_id
- description
properties:
component_id:
title: Name
type: string
description: Unique name of the component
description:
title: Description
type: string
description: Help others understand what this website is for.
- title: Some more additional info that was previously provided automatically
required:
- owner
- repoUrl
properties:
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
- title: Choose a location
repoUrl:
title: Repository Location
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
```
There are a few things to note here. On the `alpha` version, the second step of
the template flow in the frontend was provided by Backstage for free, so we used
to collect the user input for the `owner` field and the `repositoryUrl` that you
were going to publish to. Now because `actions` can have any workflow they like,
it doesn't make sense to still provide these fields for every scaffolding
workflow, as you might not need these anymore. That's why we now manually add
those fields back into the template parameters that are shown to the user:
```yaml
- title: Some more additional info that was previously provided automatically
required:
- owner
- repoUrl
properties:
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
- title: Choose a location
repoUrl:
title: Repository Location
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
```
Maybe you also don't need to publish to `github.com`, you should replace this
with your VCS provider URL that is listed in your `integrations` config instead.
### `steps`
So now we should have all the required information that we need from the user in
a much more extensible way. We now need to tell the scaffolder what to do with
these parameters and what to do with the user input.
We've made templating using `cookiecutter` a little simpler. You don't need to
store the `cookiecutter` skeleton in the same directory as the `template.yaml`
definition, it can live wherever you like - maybe a shared repository somewhere
so you can re-use the skeletons but apply different actions for different
templates depending on your use case.
We also no longer need to have a directory called
`{{cookiecutter.component_id}}`. This is because now we can't ensure that
`component_id` will be a parameter that is provided from the frontend, this
could break `cookiecutter`. If your directory structure used to look like this:
```
my-awesome-template
-> {{cookiecutter.component_id}}
-> file.txt
-> some_more_files.ts
-> hooks
-> post_gen_project.sh
-> template.yaml
```
We now recommend that you move to the following structure:
```
my-awesome-template
-> skeleton
-> file.txt
-> some_more_files.ts
-> template.yaml
```
This migration renames the skeleton folder to something more semantic, and also
drops support for `cookiecutter` hooks. We've dropped support for `cookiecutter`
hooks for now, as hopefully everything that is stored in these hooks can be
moved to `actions` instead, and for security reasons, it's more secure to run
trusted code that you ship with Backstage as an action rather than some script
that can be pulled in from anywhere which doesn't get vetted first. It's a
pretty big security risk that those scripts will be run on Backstage instances
inside your infrastructure, especially `.sh` files.
If you really need hooks and can't find a suitable solution by using actions
please reach out to us through a ticket and we'll see what we can do to assist
:)
You'll notice that we removed the `templater` property from the `spec`
definition in the template `yaml`, so there's no way to define that this is a
`cookiecutter` `templater`.
We've created a built-in action that you can use which will when run, go grab a
directory from anywhere and run `cookiecutter` on top of it, and then extract
the contents into the working directory for the scaffolder.
Adding the `steps` for a simple template should look something like the
following:
```yaml
spec:
steps:
# this action will go use cookiecutter to template some files into the working directory
- id: template # an ID for the templating step
name: Create skeleton # A user friendly name for the action
action: fetch:cookiecutter
input:
url: ./skeleton # this is the directory for your skeleton files.
# If it's located next to the `template.yaml` then you can use a relative path,
# otherwise you can use absolute URLs that point at the VCS: https://github.com/backstage/backstage/tree/master/some_folder_somewhere
values:
# for each value that you need to pass to cookiecutter, they should be listed here and set in this values object.
# You can use the handlebars templating syntax to pull them from the input parameters listed in the same file
name: '{{ parameters.name }}'
owner: '{{ parameters.owner }}'
destination: '{{ parseRepoUrl parameters.repoUrl }}'
# this action is for publishing the working directory to the VCS
- id: publish
name: Publish
action: publish:github
input:
allowedHosts: ['github.com']
description: 'This is {{ parameters.name }}'
repoUrl: '{{ parameters.repoUrl }}'
# this action will then register the created component in Backstage
- id: register
name: Register
action: catalog:register
input:
repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}'
catalogInfoPath: '/catalog-info.yaml'
```
### `output`
Steps can output values, and so can the template itself. This is good for
returning values to the frontend, so we can make the buttons like
`Go to catalog` and `Go to repo` work correctly. You can add the following to
your `template.yaml` to make sure you return the right values from the steps:
```yaml
spec:
output:
remoteUrl: '{{ steps.publish.output.remoteUrl }}'
entityRef: '{{ steps.register.output.entityRef }}'
```
Or you can return a `links` array with text and a URL explicitly:
```yaml
spec:
output:
links:
- url: '{{steps.publish.output.remoteUrl}}'
text: 'Go to Repo'
```
## Questions?
If you have any questions or feedback, please reach out to us on GitHub or
Discord and we will do our best to help!
@@ -106,7 +106,6 @@ return await createRouter({
publishers,
logger,
config,
dockerClient,
database,
catalogClient,
reader,
@@ -124,7 +123,6 @@ return await createRouter({
publishers,
logger,
config,
dockerClient,
database,
catalogClient,
reader,
@@ -136,11 +134,9 @@ return await createRouter({
want to have those as well as your new one, you'll need to do the following:
```ts
import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend`;
import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend';
const builtInActions = createBuiltinActions({
dockerClient,
integrations,
catalogClient,
templaters,
@@ -155,7 +151,6 @@ return await createRouter({
publishers,
logger,
config,
dockerClient,
database,
catalogClient,
reader,
+8 -3
View File
@@ -63,6 +63,7 @@ Create a file called `techdocs.ts` inside `packages/backend/src/plugins/` and
add the following
```typescript
import { DockerContainerRunner } from '@backstage/backend-common';
import {
createRouter,
Generators,
@@ -84,9 +85,14 @@ export default async function createPlugin({
reader,
});
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
// Generators are used for generating documentation sites.
const generators = await Generators.fromConfig(config, {
logger,
containerRunner,
});
// Publisher is used for
@@ -97,14 +103,13 @@ export default async function createPlugin({
discovery,
});
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
// checks if the publisher is working and logs the result
await publisher.getReadiness();
return await createRouter({
preparers,
generators,
publisher,
dockerClient,
logger,
config,
discovery,
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+10
View File
@@ -53,6 +53,16 @@ catalog:
token: ${GITHUB_TOKEN}
```
If Backstage is configured to use GitHub Apps authentication you must grant
`Read-Only` access for `Members` under `Organization` in order to ingest users
correctly. You can modify the app's permissions under the organization settings,
`https://github.com/organizations/{ORG}/settings/apps/{APP_NAME}/permissions`.
![permissions](./permissions.png)
**Please note that when you change permissions, the app owner will get an email
that must be approved first before the changes are applied.**
![email](./email.png)
Locations point out the specific org(s) you want to import. The `type` of these
locations must be `github-org`, and the `target` must point to the exact URL of
some organization. You can have several such location entries if you want, but
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+109 -3
View File
@@ -1,7 +1,113 @@
---
id: backend-plugin
title: Backend plugin
description: Documentation on Backend plugin
title: Backend plugins
description: Creating and Developing Backend plugins
---
## TODO
This page describes the process of creating and managing backend plugins in your
Backstage repository.
## Creating a Backend Plugin
A new, bare-bones backend plugin package can be created by issuing the following
command in your Backstage repository root:
```sh
yarn create-plugin --backend
```
Please also see the `--help` flag for the `create-plugin` command for some
further options that are available, notably the `--scope` and `--no-private`
flags that control naming and publishing of the newly created package. Your repo
root `package.json` will probably also have some default values already set up
for these.
You will be asked to supply a name for the plugin. This is an identifier that
will be part of the NPM package name, so make it short and containing only
lowercase characters separated by dashes, for example `carmen`, if it's a
package that adds an integration with a system named Carmen, for example. The
full NPM package name would then be something like
`@internal/plugin-carmen-backend`, depending on the other flags passed to the
`create-plugin` command, and your settings for the `create-plugin` command in
your root `package.json`.
Creating the plugin will take a little while, so be patient. It will helpfully
run the initial installation and build commands, so that your package is ready
to be hacked on! It will be located in a new folder in your `plugins` directory,
in this example `plugins/carmen-backend`.
For simple development purposes, a backend plugin can actually be started in a
standalone mode. You can do a first-light test of your service:
```sh
cd plugins/carmen-backend
yarn start
```
This will think for a bit, and then say `Listening on :7000`. In a different
terminal window, now run
```sh
curl localhost:7000/carmen/health
```
This should return `{"status":"ok"}`. Success! Press `Ctrl + c` to kill it
again.
## Developing your Backend Plugin
A freshly created backend plugin does basically nothing, in terms of the overall
app. It has a small set of basic dependencies and exposes an Express router in
`src/service/router.ts`. This is where you will start adding routes and
connecting those to actual underlying functionality. But nothing in your
Backstage application / backend exposes it.
To actually attach and run the plugin router, you will make some modifications
to your backend.
```sh
# From the Backstage root directory
cd packages/backend
yarn add @internal/plugin-carmen-backend@^0.1.1 # Change this to match the plugin's package.json
```
Create a new file named `packages/backend/src/plugins/carmen.ts`, and add the
following to it
```ts
import { createRouter } from '@internal/plugin-carmen-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin(env: PluginEnvironment) {
// Here is where you will add all of the required initialization code that
// your backend plugin needs to be able to start!
// The env contains a lot of goodies, but our router currently only
// needs a logger
return await createRouter({
logger: env.logger,
});
}
```
And finally, wire this into the overall backend router. Edit
`packages/backend/src/index.ts`:
```ts
import carmen from './plugins/carmen';
// ...
async function main() {
// ...
const carmenEnv = useHotMemoize(module, () => createEnv('carmen'));
apiRouter.use('/carmen', await carmen(badgesEnv));
```
After you start the backend (e.g. using `yarn start-backend` from the repo
root), you should be able to fetch data from it.
```sh
# Note the extra /api here
curl localhost:7000/api/carmen/health
```
This should return `{"status":"ok"}` like before. Success!
+2 -2
View File
@@ -329,12 +329,12 @@ above:
### 6. In the same file, modify createApp
Remember to modify the provider information based on the table above.
Remember to add or modify a `SignInPage` component for `createApp`, using
provider information based on the table above.
```tsx
const app = createApp({
apis,
plugins: Object.values(plugins),
components: {
SignInPage: props => (
<SignInPage
@@ -0,0 +1,135 @@
---
title: Where do you start when adopting Backstage?
author: Austin Lamon, Spotify
authorURL: https://www.linkedin.com/in/austinlamon
---
![Create, Manage, Explore](assets/21-05-20/create-manage-explore.gif)
One of the greatest strengths of Backstage also presents a never-ending challenge: Backstage is highly customizable and allows you to easily build a unique developer portal suited to your organizations needs. The downside of this flexibility is that it can be hard to know where to start. Backstage can do so many things — integrating every part of your tech infrastructure and developer experience — but if you set off building a developer portal without a plan, its easy to get overwhelmed by all the possibilities. To help you form your plan, this post will detail how Spotify came to design our internal portal and recommend potential models for you to use when designing and building your own.
<!--truncate-->
## Infrastructure as tech culture
Before providing recommendations on getting started with Backstage, its helpful to have a bit of context as to why Spotify made the design decisions we did. No two companies are identical — and thus, no two Backstage implementations are the same.
Rolling back the clock just a few years, [Spotify was challenged](https://engineering.atspotify.com/2021/05/18/a-product-story-the-lessons-of-backstage-and-spotifys-autonomous-culture/) to continue to scale our engineering team (and the number of features and components built) but retain the speed of product development. Some user research with Spotify developers highlighted a clear problem: there was simply too much non-documented institutional knowledge needed to get things done. No one could find anything and everyone was interrupting everyone else trying to figure things out.
Spotifys developers were facing three big challenges on a daily basis:
- They not only needed to build software quickly, they also needed to pass along knowledge to new joiners about how best to create new components.
- They needed to somehow maintain a mental model of the systems their squad owned. (Or, if they were lucky, they found a hopefully-up-to-date spreadsheet tracking this information.)
- They needed to keep an eye on what squads around them might be building to ensure they could reuse systems when they needed to solve similar problems in the future.
In short, Spotify developers needed to continue building industry leading features at breakneck speed, while simultaneously maintaining a mental model for all the software at Spotify (oh, and help every new joiner develop that mental model as well!).
<br>
## Three jobs: Create, manage, explore
Around the same time, the [jobs to be done](https://hbr.org/2016/09/know-your-customers-jobs-to-be-done) framework was becoming popular and luckily, a few Spotifiers helped guide the vision for a _sense-making tool for developers_ toward using it. After user research and many failed attempts, we landed on three jobs Spotify developers needed to do consistently:
<br>
- **Create**: Spotify developers want to delight their customers with incredible features. They create new software to do that.
- **Manage**: Spotify developers are proud of their work and treat their software and data like products. That requires maintaining all the software they own on an ongoing basis.
- **Explore**: Spotify developers want to solve new, yet unsolved problems. They try to build on existing systems to help them do that.
So: make software, maintain the software you own throughout its lifecycle, and integrate with other peoples software.
Within todays complex development environments, there are barriers both big and small that get in the way of those three jobs. Backstage provides the building blocks for removing those barriers, streamlining your development cycle, and letting developers do what they really want to do: build great features. Lets take a closer look at each of these jobs.
![Backstage Software Templates rocket](assets/21-05-20/create.gif)
### Create
**Job:** Youre an engineer, ready to start building a new microservice. Do you just pick whatever framework you want? How do you reserve capacity to run your service in production? What about managing CI/CD?
**Tool:** At Spotify, we use **Backstage Software Templates** to simplify all of this, reducing timetoHello World to just a few clicks. Instead of researching Spring Boot versus Helidon, opening a Jira ticket, rummaging through docs, and configuring CI automations, you just choose a template and your project is automatically set up in the repo for you, with the CI already running your first build.
**Result:** By making it easier to start new projects, your engineers get to the good part of coding features faster. And your organizations best practices are built into the templates, encouraging standards and reducing complexity in your tech ecosystem.
![Backstage Service Catalog system](assets/21-05-20/manage.gif)
### Manage
**Job:** Youre on a small team that owns a dozen services. Whenever you update and deploy those services, youre switching between your CI, the AWS console, a security dashboard, and a CLI so you can try to figure out which Kubernetes cluster your service ended up on. In other words, you have a lot of open windows and tabs, and each step means switching to a new interface.
**Tool:** All of your teams software components are organized together on one page in the **Backstage Service Catalog**. Go to any services page from there and its CI/CD status, [Kubernetes deployment status](https://backstage.io/blog/2021/01/12/new-backstage-feature-kubernetes-for-service-owners), documentation, security checks — and everything else related to that service — is grouped together in one seamless interface that shows you only the info you want.
**Result**: One page in Backstage has everything you need to manage the software you own. No more context-switching. No more digging through your cloud providers obscure admin features. Outside the repo and your IDE, everything you need to manage your service is right inside Backstage.
![Backstage explorer](assets/21-05-20/explore.gif)
### Explore
**Job:** You're building a new mobile feature that needs to ensure a user is paying for the premium version of your product — but someone mustve already built a library that handles that, right? A company-wide email and a few calls for help on Slack yield no response, so you resign yourself to building the capability yourself. Turns out someone did build the library you needed. They were just on vacation so they didnt see your messages. How do you enable better discovery and collaboration across your org?
**Tools:** At Spotify, anyone can find everyone elses software — because everything is centralized inside Backstage, organized by the **Backstage Service Catalog** and accessible by **search**. Go to any library or services page, and youll find the owners and documentation, even its API and how to extend it if need be.
**Result:** One place for everything, one place to search. Developers can more easily share components, build on top of each others work, and discover tools, libraries, frameworks, documentation, system designs, org charts, and more.
## Sounds great. Where do I start?
After talking with companies who have already adopted Backstage, weve seen a few common strategies for getting started. The different strategies are based on the size of your engineering org (which often also corresponds with how fast youre growing).
![200 engineers](assets/21-05-20/200-engineers.png)
<br>
### My org has ~200 engineers (and is growing fast)
Youre big enough to start feeling the pain, and only getting bigger. Onboarding and discoverability are your biggest challenges.
**Pain points:**
- This size seems to be the tipping point — where complexity is taking hold, collaboration starts breaking down, and ad hoc solutions stop working.
- Often this is also when youre growing so fast (doubling in size every 612 months) that there are now more new engineers than old engineers.
- New engineers cant find anything, so they ask around, which pulls all your other engineers off-task with interruptions.
- Logjams form. One company told us how it was taking 34 days for pull requests to get merged.
**Recommendation — Explore, then create:**
- New engineers need an easy way to find out how to do things, so youre not just adding bodies, youre adding happy, productive contributors.
- To streamline onboarding, start with centralizing your documentation and making your tools and software components discoverable in Backstage.
- At this size, you might not have a dedicated platform/infra team in place yet, but Backstage can provide the framework for centralizing and sharing knowledge — from managing compliance requirements to finding the right API documentation.
- This allows both new and old engineers to collaborate more efficiently, easily discover best practices, and cuts down on duplicated work (e.g., a new team doesnt end up rebuilding a database that already exists but nobody can find).
![1,000 engineers](assets/21-05-20/1000-engineers.png)
### My org has ~1,000 engineers
Youre officially big. Lots and lots of teams are managing lots and lots of software — and the frustration of switching between all the various tools to do that is growing exponentially.
**Pain points:**
- Fragmentation and entropy are real threats to productivity. From security requirements to cluster management to a thousand data endpoints, theres too much to keep track of, leading to cognitive overload.
- A death by a thousand cuts: constantly logging into new interfaces — from your cloud providers console to some brand new security tool then back to your CI/CD — is bogging your engineers down with too much context-switching and a lot of noise.
- Every day, its getting more and more difficult for one team to manage their own microservices, data, and documentation, let alone share their knowledge with other teams.
**Recommendation — Manage, then explore, then create:**
- Backstage will allow your teams to get a handle on the software they own, since everything they need to manage it is in one place.
- From CI/CD status to documentation to deciphering a monorepo, Backstage makes it easy to manage a service (or other software component) on a day-to-day basis.
- The service catalog also helps your developers visualize your existing software ecosystem, beyond the software they own. And with Backstage Software Templates, every new software component is already added to the catalog.
- Even at this scale, a small platform team should be all it takes to build and maintain your own version of Backstage. At Spotify, we have a 4-person team responsible for our internal version of Backstage, which is used by all of Spotifys 1,600+ developers.
![More than 1,000 engineers](assets/21-05-20/1000-plus-engineers.png)
### My org has 1,000+ engineers
Integrating infrastructure of this size and complexity can seem overwhelming. Its an even bigger challenge to bring this level of change to a well-established culture with ingrained processes.
**Pain points:**
- When youre this large, youve incorporated a lot of technology and processes, as well as a lot of partners, each with their own technologies and processes.
- Youre focussed on “replacing cruft” and bringing discoverability and order to your vast ecosystem of components and tools.
- Getting your tools adopted by your engineers while modernizing your tech stack and coordinating with your infra teams to improve your engineering practices across the whole company… its a lot.
**Recommendation — Create, then manage, then explore:**
- The fastest way to bring change to your ecosystem is to start at the beginning of the chain with Backstage Software Templates.
- With every new component created with your templates, youre establishing best practices and rewarding your developers with a streamlined experience focused on their needs — all the while building up your new service catalog.
- The more the templates ease the process of starting up a project, the more your engineers will adopt them, and the faster the other benefits of Backstage will build up, transforming productivity across your organization. ([Thats how we did it at Spotify](https://open.spotify.com/episode/7iuQ3ew1Wwpuiq6LbBKzCl).)
## More questions about adopting Backstage?
[Contact the Backstage team at Spotify.](https://calendly.com/spotify-backstage) Well share more about what weve learned from our experience here at Spotify — and from other companies who are already using Backstage to transform their developer experience.
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

@@ -0,0 +1,9 @@
---
title: GitHub Release Manager
author: '@Spotify'
authorUrl: https://github.com/spotify
category: Release management
description: Manage releases without having to juggle git commands
documentation: https://github.com/backstage/backstage/tree/master/plugins/git-release-manager
iconUrl: img/git-release-manager-logo.svg
npmPackageName: '@backstage/plugin-git-release-manager'
+15
View File
@@ -0,0 +1,15 @@
---
title: iLert
author: iLert
authorUrl: https://www.ilert.com
category: Monitoring
description: iLert is a platform for alerting, on-call management and uptime monitoring targeted at DevOps and IT teams.
documentation: https://github.com/backstage/backstage/tree/master/plugins/ilert
iconUrl: https://avatars.githubusercontent.com/u/13230510?s=200&v=4
npmPackageName: '@backstage/plugin-ilert'
tags:
- monitoring
- errors
- alerting
- uptime
- on-call
+1 -1
View File
@@ -18,7 +18,7 @@
"@spotify/prettier-config": "^10.0.0",
"docusaurus": "^2.0.0-alpha.70",
"js-yaml": "^4.1.0",
"prettier": "^2.2.1"
"prettier": "^2.3.0"
},
"prettier": "@spotify/prettier-config"
}
+1 -1
View File
@@ -431,7 +431,7 @@ class Index extends React.Component {
<Block.SmallTitle small>Pick a cloud, any cloud</Block.SmallTitle>
<Block.Paragraph>
Since Backstage uses the Kubernetes API, it's cloud agnostic
so it works no matter which cloud provide or managed Kubernetes
so it works no matter which cloud provider or managed Kubernetes
service you use, and even works in multi-cloud orgs
</Block.Paragraph>
</Block.TextBox>
+3 -1
View File
@@ -40,6 +40,7 @@
"features/software-catalog/references",
"features/software-catalog/well-known-annotations",
"features/software-catalog/well-known-relations",
"features/software-catalog/well-known-statuses",
"features/software-catalog/extending-the-model",
"features/software-catalog/external-integrations",
"features/software-catalog/software-catalog-api"
@@ -65,7 +66,8 @@
"features/software-templates/writing-templates",
"features/software-templates/builtin-actions",
"features/software-templates/writing-custom-actions",
"features/software-templates/template-legacy"
"features/software-templates/template-legacy",
"features/software-templates/migrating-from-v1alpha1-to-v1beta2"
]
},
{
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="500" version="1.1" viewBox="0 0 500 500"><title>Export</title><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="ship" fill-rule="nonzero" transform="translate(249.680798, 211.000000) scale(-1, 1) translate(-249.680798, -211.000000) translate(35.680798, -39.000000)"><g id="Flamez" transform="translate(279.925187, 455.280081) rotate(-90.000000) translate(-279.925187, -455.280081) translate(235.660848, 411.639183)"><path id="Path" fill="#F79548" d="M77.6240648,9.79348504 C64.5660848,-3.26449501 42.8017456,-3.26449501 29.7437656,9.79348504 C11.6072319,27.9300187 0.725685786,80.8882481 8.85963511e-15,83.064682 C8.85963511e-15,84.5154302 8.85963511e-15,85.241116 0.725685786,85.9668017 C2.17643392,86.6924875 3.62780549,86.6924875 5.07855362,86.6924875 C7.25498753,85.9668017 60.213217,75.810318 78.3497506,56.9487219 C91.4077307,43.8901185 91.4077307,22.8514651 77.6240648,9.79348504 Z"/><path id="Path" fill="#FCBB76" d="M75.4476309,10.5185474 C68.9183292,3.98924564 58.036783,3.98924564 51.5074813,10.5185474 C42.0766833,19.9493454 36.9981297,46.0659289 36.9981297,46.7916147 C36.9981297,47.5173005 36.9981297,48.2423628 37.7238155,48.2423628 C38.4495012,48.9680486 38.4495012,48.9680486 39.1745636,48.9680486 C40.6253117,48.9680486 66.7418953,43.1644327 75.4476309,34.458697 C81.9769327,27.9300187 81.9769327,17.0478491 75.4476309,10.5185474 Z"/></g><path id="Shape" fill="#E8E8E8" stroke="#DDD" stroke-width="2" d="M216.186409,55.2394015 C218.49257,55.2394015 220.58047,56.1742348 222.091833,57.6855981 C223.534498,59.128263 224.451854,61.0962436 224.532269,63.2779325 L224.53803,63.5910224 L224.53803,91.005836 L353.369587,261.704121 C354.323776,262.968115 354.87911,264.447677 355.019478,265.965658 C355.159883,267.484039 354.88511,269.040862 354.179061,270.458806 C353.472845,271.876805 352.395871,273.033852 351.099552,273.836436 C349.895645,274.581806 348.502551,275.021453 347.041547,275.080457 L346.703242,275.087282 L224.53803,275.087282 L224.53803,303.904613 L346.684539,303.904613 C348.308638,303.904613 349.858968,304.375354 351.174692,305.21426 C352.490519,306.053232 353.571696,307.260417 354.25731,308.732969 C354.942424,310.205367 355.16974,311.809443 354.964363,313.356104 C354.77365,314.792333 354.20983,316.179047 353.29311,317.364769 L353.075304,317.634761 L309.451382,369.468132 C307.930114,371.274871 305.719044,372.349921 303.368443,372.436371 L303.061097,372.44202 L124.602244,372.44202 C122.240117,372.44202 119.994057,371.442358 118.41433,369.699238 L118.212103,369.467679 L74.5886595,317.634759 C73.5428392,316.392134 72.9047814,314.903039 72.6994228,313.356514 C72.494029,311.809724 72.7214803,310.205497 73.4066556,308.732969 C74.0917295,307.260659 75.1726391,306.053772 76.4882065,305.214939 C77.7099169,304.435952 79.1339993,303.974371 80.6313862,303.912406 L80.9781796,303.905237 L207.834788,303.905237 L207.834788,274.720698 L106.28616,274.720698 C103.979999,274.720698 101.892098,273.785865 100.380735,272.274502 C98.8693719,270.763138 97.9345387,268.675238 97.9345387,266.369077 C97.9345387,233.249109 111.496611,180.759139 130.891816,138.720038 C142.0216,114.595575 154.255888,95.1994886 167.266567,81.0803824 C183.02715,63.976526 199.471354,55.2394015 216.186409,55.2394015 Z M328.739655,320.607855 L98.9224399,320.607855 L128.488453,355.738155 L299.173642,355.738155 L328.739655,320.607855 Z M224.53803,118.732589 L224.53803,258.384663 L329.936488,258.384663 L224.53803,118.732589 Z M207.834165,73.0839035 C187.31365,78.5957312 165.169959,104.292863 146.058313,145.71761 C129.135086,182.399574 116.887708,227.020442 114.917014,258.016833 L114.917014,258.016833 L207.834165,258.016833 Z" transform="translate(213.840829, 213.840711) rotate(45.000000) translate(-213.840829, -213.840711)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+4 -4
View File
@@ -5197,10 +5197,10 @@ prepend-http@^2.0.0:
resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
prettier@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
prettier@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
prismjs@^1.22.0:
version "1.23.0"
+3
View File
@@ -37,6 +37,7 @@ nav:
- Entity References: 'features/software-catalog/references.md'
- Well-known Annotations: 'features/software-catalog/well-known-annotations.md'
- Well-known Relations: 'features/software-catalog/well-known-relations.md'
- Well-known Statuses: 'features/software-catalog/well-known-statuses.md'
- Extending the model: 'features/software-catalog/extending-the-model.md'
- External integrations: 'features/software-catalog/external-integrations.md'
- API: 'features/software-catalog/api.md'
@@ -53,6 +54,8 @@ nav:
- Builtin Actions: 'features/software-templates/builtin-actions.md'
- Writing Custom Actions: 'features/software-templates/writing-custom-actions.md'
- Writing Templates (Legacy): 'features/software-templates/legacy.md'
- Migrating from v1alpha1 to v1beta2 templates: 'features/software-templates/migrating-from-v1alpha1-to-v1beta2.md'
- Backstage Search:
- Overview: 'features/search/README.md'
- Search Architecture: 'features/search/architecture.md'
+4 -8
View File
@@ -30,7 +30,8 @@
"prettier:check": "prettier --check .",
"lerna": "lerna",
"storybook": "yarn workspace storybook start",
"build-storybook": "yarn workspace storybook build-storybook"
"build-storybook": "yarn workspace storybook build-storybook",
"prepare": "husky install"
},
"workspaces": {
"packages": [
@@ -48,8 +49,8 @@
},
"version": "1.0.0",
"dependencies": {
"@microsoft/api-extractor": "7.13.2-pr1916.0",
"@microsoft/api-documenter": "^7.12.16",
"@microsoft/api-extractor": "7.13.2-pr1916.0",
"@microsoft/api-extractor-model": "^7.12.5"
},
"devDependencies": {
@@ -60,18 +61,13 @@
"command-exists": "^1.2.9",
"concurrently": "^6.0.0",
"fs-extra": "^9.0.0",
"husky": "^4.2.3",
"husky": "^6.0.0",
"lerna": "^4.0.0",
"lint-staged": "^10.1.0",
"prettier": "^2.2.1",
"recursive-readdir": "^2.2.2",
"shx": "^0.3.2"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"prettier": "@spotify/prettier-config",
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
+112
View File
@@ -1,5 +1,117 @@
# example-app
## 0.2.29
### Patch Changes
- Updated dependencies [7cbfcae48]
- Updated dependencies [2bfec55a6]
- Updated dependencies [f7f7783a3]
- Updated dependencies [65e6c4541]
- Updated dependencies [68fdbf014]
- Updated dependencies [5da6a561d]
- Updated dependencies [ca6e0ab69]
- Updated dependencies [5914a76d5]
- Updated dependencies [81d7b9c6f]
- Updated dependencies [a62cfe068]
- Updated dependencies [35e091604]
- Updated dependencies [a53f3d603]
- Updated dependencies [b203699e9]
- @backstage/plugin-cost-insights@0.10.0
- @backstage/cli@0.6.12
- @backstage/catalog-model@0.7.10
- @backstage/plugin-scaffolder@0.9.5
- @backstage/core@0.7.10
- @backstage/plugin-api-docs@0.4.14
- @backstage/plugin-cloudbuild@0.2.15
- @backstage/plugin-github-actions@0.4.7
- @backstage/plugin-techdocs@0.9.3
- @backstage/plugin-catalog-import@0.5.7
- @backstage/plugin-catalog@0.5.8
## 0.2.28
### Patch Changes
- Updated dependencies [062bbf90f]
- Updated dependencies [2cd70e164]
- Updated dependencies [0b033d07b]
- Updated dependencies [3be844496]
- Updated dependencies [5542de095]
- Updated dependencies [10c008a3a]
- Updated dependencies [81ef1d57b]
- Updated dependencies [ea21d46f0]
- Updated dependencies [e3fc89df6]
- Updated dependencies [f59a945b7]
- Updated dependencies [889d89b6e]
- Updated dependencies [16be1d093]
- Updated dependencies [3f988cb63]
- Updated dependencies [675a569a9]
- @backstage/core@0.7.9
- @backstage/integration-react@0.1.2
- @backstage/plugin-api-docs@0.4.13
- @backstage/plugin-badges@0.2.1
- @backstage/plugin-catalog@0.5.7
- @backstage/plugin-catalog-import@0.5.6
- @backstage/plugin-catalog-react@0.1.6
- @backstage/plugin-circleci@0.2.14
- @backstage/plugin-cloudbuild@0.2.14
- @backstage/plugin-code-coverage@0.1.3
- @backstage/plugin-cost-insights@0.9.1
- @backstage/plugin-explore@0.3.5
- @backstage/plugin-gcp-projects@0.2.6
- @backstage/plugin-github-actions@0.4.6
- @backstage/plugin-graphiql@0.2.11
- @backstage/plugin-jenkins@0.4.3
- @backstage/plugin-kafka@0.2.7
- @backstage/plugin-kubernetes@0.4.4
- @backstage/plugin-lighthouse@0.2.16
- @backstage/plugin-newrelic@0.2.7
- @backstage/plugin-org@0.3.13
- @backstage/plugin-pagerduty@0.3.4
- @backstage/plugin-rollbar@0.3.5
- @backstage/plugin-scaffolder@0.9.4
- @backstage/plugin-search@0.3.6
- @backstage/plugin-sentry@0.3.10
- @backstage/plugin-shortcuts@0.1.2
- @backstage/plugin-tech-radar@0.3.11
- @backstage/plugin-techdocs@0.9.2
- @backstage/plugin-todo@0.1.1
- @backstage/plugin-user-settings@0.2.10
- @backstage/cli@0.6.11
- @backstage/catalog-model@0.7.9
## 0.2.27
### Patch Changes
- Updated dependencies [6f1b82b14]
- Updated dependencies [38ca05168]
- Updated dependencies [f65adcde7]
- Updated dependencies [81c54d1f2]
- Updated dependencies [80888659b]
- Updated dependencies [7b8272fb7]
- Updated dependencies [8aedbb4af]
- Updated dependencies [fc79a6dd3]
- Updated dependencies [f53fba29f]
- Updated dependencies [b2e2ec753]
- Updated dependencies [9314a8592]
- Updated dependencies [2e05277e0]
- Updated dependencies [4075c6367]
- Updated dependencies [d8b81fd28]
- @backstage/plugin-cost-insights@0.9.0
- @backstage/plugin-catalog-import@0.5.5
- @backstage/plugin-github-actions@0.4.5
- @backstage/cli@0.6.10
- @backstage/core@0.7.8
- @backstage/plugin-catalog-react@0.1.5
- @backstage/theme@0.2.7
- @backstage/plugin-kubernetes@0.4.3
- @backstage/plugin-tech-radar@0.3.10
- @backstage/plugin-scaffolder@0.9.3
- @backstage/plugin-techdocs@0.9.1
- @backstage/catalog-model@0.7.8
## 0.2.26
### Patch Changes
+40 -39
View File
@@ -1,47 +1,48 @@
{
"name": "example-app",
"version": "0.2.26",
"version": "0.2.29",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.7.7",
"@backstage/cli": "^0.6.9",
"@backstage/core": "^0.7.7",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-api-docs": "^0.4.12",
"@backstage/plugin-badges": "^0.2.0",
"@backstage/plugin-catalog": "^0.5.6",
"@backstage/plugin-catalog-import": "^0.5.4",
"@backstage/plugin-catalog-react": "^0.1.3",
"@backstage/plugin-circleci": "^0.2.13",
"@backstage/plugin-cloudbuild": "^0.2.13",
"@backstage/plugin-code-coverage": "^0.1.2",
"@backstage/plugin-cost-insights": "^0.8.5",
"@backstage/plugin-explore": "^0.3.4",
"@backstage/plugin-gcp-projects": "^0.2.5",
"@backstage/plugin-github-actions": "^0.4.4",
"@backstage/plugin-graphiql": "^0.2.10",
"@backstage/plugin-jenkins": "^0.4.2",
"@backstage/plugin-kafka": "^0.2.6",
"@backstage/plugin-kubernetes": "^0.4.2",
"@backstage/plugin-lighthouse": "^0.2.15",
"@backstage/plugin-newrelic": "^0.2.6",
"@backstage/plugin-org": "^0.3.12",
"@backstage/plugin-pagerduty": "0.3.3",
"@backstage/plugin-rollbar": "^0.3.4",
"@backstage/plugin-scaffolder": "^0.9.2",
"@backstage/plugin-search": "^0.3.5",
"@backstage/plugin-sentry": "^0.3.9",
"@backstage/plugin-tech-radar": "^0.3.9",
"@backstage/plugin-techdocs": "^0.9.0",
"@backstage/plugin-todo": "^0.1.0",
"@backstage/plugin-user-settings": "^0.2.8",
"@backstage/theme": "^0.2.6",
"@backstage/catalog-model": "^0.7.10",
"@backstage/cli": "^0.6.12",
"@backstage/core": "^0.7.10",
"@backstage/integration-react": "^0.1.2",
"@backstage/plugin-api-docs": "^0.4.14",
"@backstage/plugin-badges": "^0.2.1",
"@backstage/plugin-catalog": "^0.5.8",
"@backstage/plugin-catalog-import": "^0.5.7",
"@backstage/plugin-catalog-react": "^0.1.6",
"@backstage/plugin-circleci": "^0.2.14",
"@backstage/plugin-cloudbuild": "^0.2.15",
"@backstage/plugin-code-coverage": "^0.1.3",
"@backstage/plugin-cost-insights": "^0.10.0",
"@backstage/plugin-explore": "^0.3.5",
"@backstage/plugin-gcp-projects": "^0.2.6",
"@backstage/plugin-github-actions": "^0.4.7",
"@backstage/plugin-graphiql": "^0.2.11",
"@backstage/plugin-jenkins": "^0.4.3",
"@backstage/plugin-kafka": "^0.2.7",
"@backstage/plugin-kubernetes": "^0.4.4",
"@backstage/plugin-lighthouse": "^0.2.16",
"@backstage/plugin-newrelic": "^0.2.7",
"@backstage/plugin-org": "^0.3.13",
"@backstage/plugin-pagerduty": "0.3.4",
"@backstage/plugin-rollbar": "^0.3.5",
"@backstage/plugin-scaffolder": "^0.9.5",
"@backstage/plugin-search": "^0.3.6",
"@backstage/plugin-sentry": "^0.3.10",
"@backstage/plugin-shortcuts": "^0.1.2",
"@backstage/plugin-tech-radar": "^0.3.11",
"@backstage/plugin-techdocs": "^0.9.3",
"@backstage/plugin-todo": "^0.1.1",
"@backstage/plugin-user-settings": "^0.2.10",
"@backstage/theme": "^0.2.7",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.5.3",
"@roadiehq/backstage-plugin-buildkite": "^1.0.0",
"@roadiehq/backstage-plugin-github-insights": "^1.0.0",
"@roadiehq/backstage-plugin-github-insights": "^1.1.10",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.0.0",
"@roadiehq/backstage-plugin-travis-ci": "^1.0.0",
"history": "^5.0.0",
@@ -51,22 +52,22 @@
"react-hot-loader": "^4.12.21",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3",
"react-use": "^17.2.4",
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/test-utils": "^0.1.10",
"@backstage/test-utils": "^0.1.12",
"@testing-library/cypress": "^7.0.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.0.7",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "^26.0.7",
"@types/jquery": "^3.3.34",
"@types/node": "^14.14.32",
"@types/react-dom": "^16.9.8",
"@types/zen-observable": "^0.8.0",
"cross-env": "^7.0.0",
"cypress": "^4.2.0",
"cypress": "^7.3.0",
"eslint-plugin-cypress": "^2.10.3",
"start-server-and-test": "^1.10.11"
},
@@ -39,6 +39,7 @@ import { NavLink } from 'react-router-dom';
import { GraphiQLIcon } from '@backstage/plugin-graphiql';
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
import { SidebarSearch } from '@backstage/plugin-search';
import { Shortcuts } from '@backstage/plugin-shortcuts';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -91,6 +92,8 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarItem icon={RuleIcon} to="lighthouse" text="Lighthouse" />
<SidebarItem icon={MoneyIcon} to="cost-insights" text="Cost Insights" />
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
<SidebarDivider />
<Shortcuts />
<SidebarSpace />
<SidebarDivider />
<SidebarSettings />
@@ -28,6 +28,8 @@ import {
import { EntityBadgesDialog } from '@backstage/plugin-badges';
import {
EntityAboutCard,
EntityDependsOnComponentsCard,
EntityDependsOnResourcesCard,
EntityHasComponentsCard,
EntityHasSubcomponentsCard,
EntityHasSystemsCard,
@@ -37,6 +39,9 @@ import {
EntitySwitch,
isComponentType,
isKind,
EntityHasResourcesCard,
EntityOrphanWarning,
isOrphan,
} from '@backstage/plugin-catalog';
import {
EntityCircleCIContent,
@@ -211,7 +216,15 @@ const errorsContent = (
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntitySwitch>
<EntitySwitch.Case if={isOrphan}>
<Grid item xs={12}>
<EntityOrphanWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={8} xs={12}>
<EntityAboutCard variant="gridItem" />
</Grid>
@@ -223,7 +236,7 @@ const overviewContent = (
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={4} sm={6}>
<Grid item md={4} xs={12}>
<EntityLinksCard />
</Grid>
@@ -257,7 +270,7 @@ const overviewContent = (
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={6}>
<Grid item md={8} xs={12}>
<EntityHasSubcomponentsCard variant="gridItem" />
</Grid>
</Grid>
@@ -288,6 +301,17 @@ const serviceEntityPage = (
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
@@ -336,6 +360,17 @@ const websiteEntityPage = (
{errorsContent}
</EntityLayout.Route>
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
@@ -466,6 +501,9 @@ const systemPage = (
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/diagram" title="Diagram">
+1
View File
@@ -17,3 +17,4 @@
// TODO(Rugvip): This plugin is currently not part of the app element tree,
// ideally we have an API for the context menu that permits that.
export { badgesPlugin } from '@backstage/plugin-badges';
export { shortcutsPlugin } from '@backstage/plugin-shortcuts';
+150
View File
@@ -1,5 +1,155 @@
# @backstage/backend-common
## 0.8.1
### Patch Changes
- c7dad9218: All cache-related connection errors are now handled and logged by the cache manager. App Integrators may provide an optional error handler when instantiating the cache manager if custom error handling is needed.
```typescript
// Providing an error handler
const cacheManager = CacheManager.fromConfig(config, {
onError: e => {
if (isSomehowUnrecoverable(e)) {
gracefullyShutThingsDown();
process.exit(1);
}
},
});
```
- 65e6c4541: Remove circular dependencies
- 5001de908: Change GitlabUrlReader to SHA timestamp compare using only commits that modify given file path, if file path given
- Updated dependencies [65e6c4541]
- Updated dependencies [290405276]
- @backstage/integration@0.5.3
- @backstage/config-loader@0.6.2
## 0.8.0
### Minor Changes
- 22fd8ce2a: Introducing: a standard API for App Integrators to configure cache stores and Plugin Developers to interact with them.
Two cache stores are currently supported.
- `memory`, which is a very simple in-memory key/value store, intended for local development.
- `memcache`, which can be used to connect to a memcache host.
Configuring and working with cache stores is very similar to the process for database connections.
```yaml
backend:
cache:
store: memcache
connection: user:pass@cache.example.com:11211
```
```typescript
import { CacheManager } from '@backstage/backend-common';
// Instantiating a cache client for a plugin.
const cacheManager = CacheManager.fromConfig(config);
const somePluginCache = cacheManager.forPlugin('somePlugin');
const cacheClient = somePluginCache.getClient();
// Using the cache client:
const cachedValue = await cacheClient.get('someKey');
if (cachedValue) {
return cachedValue;
} else {
const someValue = await someExpensiveProcess();
await cacheClient.set('someKey', someValue);
}
await cacheClient.delete('someKey');
```
Cache clients deal with TTLs in milliseconds. A TTL can be provided as a defaultTtl when getting a client, or may be passed when setting specific objects. If no TTL is provided, data will be persisted indefinitely.
```typescript
// Getting a client with a default TTL
const cacheClient = somePluginCache.getClient({
defaultTtl: 3600000,
});
// Setting a TTL on a per-object basis.
cacheClient.set('someKey', data, { ttl: 3600000 });
```
Configuring a cache store is optional. Even when no cache store is configured, the cache manager will dutifully pass plugins a manager that resolves a cache client that does not actually write or read any data.
### Patch Changes
- f9fb4a205: Prep work for mysql support in backend-common
## 0.7.0
### Minor Changes
- e0bfd3d44: Refactor the `runDockerContainer(…)` function to an interface-based api.
This gives the option to replace the docker runtime in the future.
Packages and plugins that previously used the `dockerode` as argument should be migrated to use the new `ContainerRunner` interface instead.
```diff
import {
- runDockerContainer,
+ ContainerRunner,
PluginEndpointDiscovery,
} from '@backstage/backend-common';
- import Docker from 'dockerode';
type RouterOptions = {
// ...
- dockerClient: Docker,
+ containerRunner: ContainerRunner;
};
export async function createRouter({
// ...
- dockerClient,
+ containerRunner,
}: RouterOptions): Promise<express.Router> {
// ...
+ await containerRunner.runContainer({
- await runDockerContainer({
image: 'docker',
// ...
- dockerClient,
});
// ...
}
```
To keep the `dockerode` based runtime, use the `DockerContainerRunner` implementation:
```diff
+ import {
+ ContainerRunner,
+ DockerContainerRunner
+ } from '@backstage/backend-common';
- import { runDockerContainer } from '@backstage/backend-common';
+ const containerRunner: ContainerRunner = new DockerContainerRunner({dockerClient});
+ await containerRunner.runContainer({
- await runDockerContainer({
image: 'docker',
// ...
- dockerClient,
});
```
### Patch Changes
- 38ca05168: The default `@octokit/rest` dependency was bumped to `"^18.5.3"`.
- Updated dependencies [38ca05168]
- Updated dependencies [d8b81fd28]
- @backstage/integration@0.5.2
- @backstage/config-loader@0.6.1
- @backstage/config@0.1.5
## 0.6.3
### Patch Changes
+43 -4
View File
@@ -16,6 +16,7 @@ import { GithubCredentialsProvider } from '@backstage/integration';
import { GitHubIntegration } from '@backstage/integration';
import { GitLabIntegration } from '@backstage/integration';
import * as http from 'http';
import { JsonValue } from '@backstage/config';
import { Knex } from 'knex';
import { Logger } from 'winston';
import { MergeResult } from 'isomorphic-git';
@@ -62,9 +63,28 @@ export class BitbucketUrlReader implements UrlReader {
toString(): string;
}
// @public
export interface CacheClient {
delete(key: string): Promise<void>;
get(key: string): Promise<JsonValue | undefined>;
set(key: string, value: JsonValue, options?: CacheSetOptions): Promise<void>;
}
// @public
export class CacheManager {
forPlugin(pluginId: string): PluginCacheManager;
static fromConfig(config: Config, options?: CacheManagerOptions): CacheManager;
}
// @public (undocumented)
export const coloredFormat: winston.Logform.Format;
// @public (undocumented)
export interface ContainerRunner {
// (undocumented)
runContainer(opts: RunContainerOptions): Promise<void>;
}
// @public @deprecated
export const createDatabase: typeof createDatabaseClient;
@@ -80,6 +100,15 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl;
// @public (undocumented)
export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise<express.Router>;
// @public (undocumented)
export class DockerContainerRunner implements ContainerRunner {
constructor({ dockerClient }: {
dockerClient: Docker;
});
// (undocumented)
runContainer({ imageName, command, args, logStream, mountDirs, workingDir, envVars, }: RunContainerOptions): Promise<void>;
}
// @public
export function ensureDatabaseExists(dbConfig: Config, ...databases: Array<string>): Promise<void>;
@@ -223,6 +252,11 @@ export function loadBackendConfig(options: Options): Promise<Config>;
// @public
export function notFoundHandler(): RequestHandler;
// @public
export type PluginCacheManager = {
getClient: (options?: ClientOptions) => CacheClient;
};
// @public
export interface PluginDatabaseManager {
getClient(): Promise<Knex>;
@@ -255,10 +289,15 @@ export function requestLoggingHandler(logger?: Logger): RequestHandler;
export function resolvePackagePath(name: string, ...paths: string[]): string;
// @public (undocumented)
export const runDockerContainer: ({ imageName, args, logStream, dockerClient, mountDirs, workingDir, envVars, createOptions, }: RunDockerContainerOptions) => Promise<{
error: any;
statusCode: any;
}>;
export type RunContainerOptions = {
imageName: string;
command?: string | string[];
args: string[];
logStream?: Writable;
mountDirs?: Record<string, string>;
workingDir?: string;
envVars?: Record<string, string>;
};
// @public
export type SearchResponse = {
+14
View File
@@ -68,6 +68,20 @@ export interface Config {
connection: string | object;
};
/** Cache connection configuration, select cache type using the `store` field */
cache?:
| {
store: 'memory';
}
| {
store: 'memcache';
/**
* A memcache connection string in the form `user:pass@host:port`.
* @secret
*/
connection: string;
};
cors?: {
origin?: string | string[];
methods?: string | string[];
+11 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.6.3",
"version": "0.8.1",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -30,10 +30,10 @@
},
"dependencies": {
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.4",
"@backstage/config-loader": "^0.6.0",
"@backstage/config": "^0.1.5",
"@backstage/config-loader": "^0.6.2",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@backstage/integration": "^0.5.3",
"@google-cloud/storage": "^5.8.0",
"@octokit/rest": "^18.5.3",
"@types/cors": "^2.8.6",
@@ -51,6 +51,8 @@
"git-url-parse": "^11.4.4",
"helmet": "^4.0.0",
"isomorphic-git": "^1.8.0",
"keyv": "^4.0.3",
"keyv-memcache": "^1.2.5",
"knex": "^0.95.1",
"lodash": "^4.17.15",
"logform": "^2.1.1",
@@ -62,7 +64,8 @@
"stoppable": "^1.1.0",
"tar": "^6.0.5",
"unzipper": "^0.10.11",
"winston": "^3.2.1"
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"peerDependencies": {
"pg-connection-string": "^2.3.0"
@@ -73,8 +76,8 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.6.9",
"@backstage/test-utils": "^0.1.10",
"@backstage/cli": "^0.6.12",
"@backstage/test-utils": "^0.1.12",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
@@ -94,6 +97,7 @@
"jest": "^26.0.1",
"mock-fs": "^4.13.0",
"msw": "^0.21.2",
"mysql2": "^2.2.5",
"recursive-readdir": "^2.2.2",
"supertest": "^6.1.3"
},
+126
View File
@@ -0,0 +1,126 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { DefaultCacheClient } from './CacheClient';
import Keyv from 'keyv';
describe('CacheClient', () => {
let client: Keyv;
const b64 = (k: string) => Buffer.from(k).toString('base64');
beforeEach(() => {
client = new Keyv();
client.get = jest.fn();
client.set = jest.fn();
client.delete = jest.fn();
});
afterEach(() => jest.resetAllMocks());
describe('CacheClient.get', () => {
it('calls client with normalized key', async () => {
const sut = new DefaultCacheClient({ client });
const key = 'somekey';
await sut.get(key);
expect(client.get).toHaveBeenCalledWith(b64(key));
});
it('calls client with normalized key (very long key)', async () => {
const sut = new DefaultCacheClient({ client });
const key = 'x'.repeat(251);
await sut.get(key);
const spy = client.get as jest.Mock;
const actualKey = spy.mock.calls[0][0];
expect(actualKey).not.toEqual(b64(key));
expect(actualKey.length).toBeLessThan(250);
});
it('rejects on underlying error', async () => {
const sut = new DefaultCacheClient({ client });
const expectedError = new Error('Some runtime error');
client.get = jest.fn().mockRejectedValue(expectedError);
return expect(sut.get('someKey')).rejects.toEqual(expectedError);
});
it('resolves what underlying client resolves', async () => {
const sut = new DefaultCacheClient({ client });
const expectedValue = 'some value';
client.get = jest.fn().mockResolvedValue(expectedValue);
const actualValue = await sut.get('someKey');
return expect(actualValue).toEqual(expectedValue);
});
});
describe('CacheClient.set', () => {
it('calls client with normalized key', async () => {
const sut = new DefaultCacheClient({ client });
const key = 'somekey';
await sut.set(key, {});
const spy = client.set as jest.Mock;
const actualKey = spy.mock.calls[0][0];
expect(actualKey).toEqual(b64(key));
});
it('passes ttl to client when given', async () => {
const sut = new DefaultCacheClient({ client });
const expectedTtl = 3600;
await sut.set('someKey', {}, { ttl: expectedTtl });
const spy = client.set as jest.Mock;
const actualTtl = spy.mock.calls[0][2];
expect(actualTtl).toEqual(expectedTtl);
});
it('rejects on underlying error if configured', async () => {
const sut = new DefaultCacheClient({ client });
const expectedError = new Error('Some runtime error');
client.set = jest.fn().mockRejectedValue(expectedError);
return expect(sut.set('someKey', {})).rejects.toEqual(expectedError);
});
});
describe('CacheClient.delete', () => {
it('calls client with normalized key', async () => {
const sut = new DefaultCacheClient({ client });
const key = 'somekey';
await sut.delete(key);
const spy = client.delete as jest.Mock;
const actualKey = spy.mock.calls[0][0];
expect(actualKey).toEqual(b64(key));
});
it('rejects on underlying error if configured', async () => {
const sut = new DefaultCacheClient({ client });
const expectedError = new Error('Some runtime error');
client.delete = jest.fn().mockRejectedValue(expectedError);
return expect(sut.delete('someKey')).rejects.toEqual(expectedError);
});
});
});
+102
View File
@@ -0,0 +1,102 @@
/*
* Copyright 2021 Spotify AB
*
* 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/config';
import { createHash } from 'crypto';
import Keyv from 'keyv';
type CacheClientArgs = {
client: Keyv;
};
type CacheSetOptions = {
/**
* Optional TTL in milliseconds. Defaults to the TTL provided when the client
* was set up (or no TTL if none are provided).
*/
ttl?: number;
};
/**
* A pre-configured, storage agnostic cache client suitable for use by
* Backstage plugins.
*/
export interface CacheClient {
/**
* Reads data from a cache store for the given key. If no data was found,
* returns undefined.
*/
get(key: string): Promise<JsonValue | undefined>;
/**
* Writes the given data to a cache store, associated with the given key. An
* optional TTL may also be provided, otherwise it defaults to the TTL that
* was provided when the client was instantiated.
*/
set(key: string, value: JsonValue, options?: CacheSetOptions): Promise<void>;
/**
* Removes the given key from the cache store.
*/
delete(key: string): Promise<void>;
}
/**
* A basic, concrete implementation of the CacheClient, suitable for almost
* all uses in Backstage.
*/
export class DefaultCacheClient implements CacheClient {
private readonly client: Keyv;
constructor({ client }: CacheClientArgs) {
this.client = client;
}
async get(key: string): Promise<JsonValue | undefined> {
const k = this.getNormalizedKey(key);
return await this.client.get(k);
}
async set(
key: string,
value: JsonValue,
opts: CacheSetOptions = {},
): Promise<void> {
const k = this.getNormalizedKey(key);
await this.client.set(k, value, opts.ttl);
}
async delete(key: string): Promise<void> {
const k = this.getNormalizedKey(key);
await this.client.delete(k);
}
/**
* Ensures keys are well-formed for any/all cache stores.
*/
private getNormalizedKey(candidateKey: string): string {
// Remove potentially invalid characters.
const wellFormedKey = Buffer.from(candidateKey).toString('base64');
// Memcache in particular doesn't do well with keys > 250 bytes.
// Padded because a plugin ID is also prepended to the key.
if (wellFormedKey.length < 200) {
return wellFormedKey;
}
return createHash('md5').update(candidateKey).digest('base64');
}
}
+225
View File
@@ -0,0 +1,225 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import Keyv from 'keyv';
/* @ts-expect-error */
import KeyvMemcache from 'keyv-memcache';
import { DefaultCacheClient } from './CacheClient';
import { CacheManager } from './CacheManager';
import { NoStore } from './NoStore';
jest.createMockFromModule('keyv');
jest.mock('keyv');
jest.createMockFromModule('keyv-memcache');
jest.mock('keyv-memcache');
jest.mock('./CacheClient', () => {
return {
DefaultCacheClient: jest.fn(),
};
});
describe('CacheManager', () => {
const defaultConfigOptions = {
backend: {
cache: {
store: 'memory',
},
},
};
const defaultConfig = () => new ConfigReader(defaultConfigOptions);
afterEach(() => jest.resetAllMocks());
describe('CacheManager.fromConfig', () => {
it('accesses the backend.cache key', () => {
const getOptionalString = jest.fn();
const config = defaultConfig();
config.getOptionalString = getOptionalString;
CacheManager.fromConfig(config);
expect(getOptionalString.mock.calls[0][0]).toEqual('backend.cache.store');
expect(getOptionalString.mock.calls[1][0]).toEqual(
'backend.cache.connection',
);
});
it('does not require the backend.cache key', () => {
const config = new ConfigReader({ backend: {} });
expect(() => {
CacheManager.fromConfig(config);
}).not.toThrowError();
});
it('throws on unknown cache store', () => {
const config = new ConfigReader({
backend: { cache: { store: 'notreal' } },
});
expect(() => {
CacheManager.fromConfig(config);
}).toThrowError();
});
});
describe('CacheManager.forPlugin', () => {
const manager = CacheManager.fromConfig(defaultConfig());
it('connects to a cache store scoped to the plugin', async () => {
const pluginId = 'test1';
manager.forPlugin(pluginId).getClient();
const client = DefaultCacheClient as jest.Mock;
expect(client).toHaveBeenCalledTimes(1);
});
it('attaches error handler to client', () => {
const pluginId = 'error-test';
manager.forPlugin(pluginId).getClient();
const client = DefaultCacheClient as jest.Mock;
const mockCalls = client.mock.calls.splice(-1);
const realClient = mockCalls[0][0].client as Keyv;
expect(realClient.on).toHaveBeenCalledWith('error', expect.any(Function));
});
it('provides different plugins different cache clients', async () => {
const plugin1Id = 'test1';
const plugin2Id = 'test2';
const expectedTtl = 3600;
manager.forPlugin(plugin1Id).getClient({ defaultTtl: expectedTtl });
manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl });
const client = DefaultCacheClient as jest.Mock;
const cache = (Keyv as unknown) as jest.Mock;
expect(cache).toHaveBeenCalledTimes(2);
expect(client).toHaveBeenCalledTimes(2);
const plugin1CallArgs = cache.mock.calls[0];
const plugin2CallArgs = cache.mock.calls[1];
expect(plugin1CallArgs[0].namespace).not.toEqual(
plugin2CallArgs[0].namespace,
);
});
});
describe('CacheManager.forPlugin stores', () => {
it('returns none client when no cache is configured', () => {
const manager = CacheManager.fromConfig(
new ConfigReader({ backend: {} }),
);
const expectedNamespace = 'test-plugin';
manager.forPlugin(expectedNamespace).getClient();
const cache = (Keyv as unknown) as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0].store).toBeInstanceOf(NoStore);
});
it('returns memory client when explicitly configured', () => {
const manager = CacheManager.fromConfig(defaultConfig());
const expectedTtl = 3600;
const expectedNamespace = 'test-plugin';
manager
.forPlugin(expectedNamespace)
.getClient({ defaultTtl: expectedTtl });
const cache = (Keyv as unknown) as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0]).toMatchObject({
ttl: expectedTtl,
namespace: expectedNamespace,
});
});
it('returns a memcache client when configured', () => {
const expectedHost = '127.0.0.1:11211';
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'memcache',
connection: expectedHost,
},
},
}),
);
const expectedTtl = 3600;
manager.forPlugin('test').getClient({ defaultTtl: expectedTtl });
const cache = (Keyv as unknown) as jest.Mock;
const mockCacheCalls = cache.mock.calls.splice(-1);
expect(mockCacheCalls[0][0]).toMatchObject({
ttl: expectedTtl,
});
expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvMemcache);
const memcache = KeyvMemcache as jest.Mock;
const mockMemcacheCalls = memcache.mock.calls.splice(-1);
expect(mockMemcacheCalls[0][0]).toEqual(expectedHost);
});
});
describe('connection errors', () => {
it('uses provided logger', () => {
// Set up and inject mock logger.
const mockLogger = { child: jest.fn(), error: jest.fn() };
mockLogger.child.mockImplementation(() => mockLogger as any);
const manager = CacheManager.fromConfig(defaultConfig(), {
logger: mockLogger as any,
});
// Set up a cache client using the configured manager.
manager.forPlugin('error-logger-test').getClient();
// Retrieve the error handler attached to the cache client.
const client = DefaultCacheClient as jest.Mock;
const mockCalls = client.mock.calls.splice(-1);
const realClient = mockCalls[0][0].client as Keyv;
const realOnError = realClient.on as jest.Mock;
const realHandler = realOnError.mock.calls.splice(-1)[0][1];
// Invoke the actual error handler.
const expectedError = new Error('some error');
realHandler(expectedError);
expect(mockLogger.error).toHaveBeenCalledWith(expectedError);
});
it('calls provided handler', () => {
// Set up and inject mock logger.
const mockHandler = jest.fn();
const manager = CacheManager.fromConfig(defaultConfig(), {
onError: mockHandler,
});
// Set up a cache client using the configured manager.
manager.forPlugin('error-handler-test').getClient();
// Retrieve the error handler attached to the cache client.
const client = DefaultCacheClient as jest.Mock;
const mockCalls = client.mock.calls.splice(-1);
const realClient = mockCalls[0][0].client as Keyv;
const realOnError = realClient.on as jest.Mock;
const realHandler = realOnError.mock.calls.splice(-1)[0][1];
// Invoke the actual error handler.
const expectedError = new Error('some error');
realHandler(expectedError);
expect(mockHandler).toHaveBeenCalledWith(expectedError);
});
});
});
+147
View File
@@ -0,0 +1,147 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Config } from '@backstage/config';
import Keyv from 'keyv';
// @ts-expect-error
import KeyvMemcache from 'keyv-memcache';
import { Logger } from 'winston';
import { getRootLogger } from '../logging';
import { DefaultCacheClient, CacheClient } from './CacheClient';
import { NoStore } from './NoStore';
import {
CacheManagerOptions,
OptionalOnError,
PluginCacheManager,
} from './types';
/**
* Implements a Cache Manager which will automatically create new cache clients
* for plugins when requested. All requested cache clients are created with the
* connection details provided.
*/
export class CacheManager {
/**
* Keys represent supported `backend.cache.store` values, mapped to factories
* that return Keyv instances appropriate to the store.
*/
private readonly storeFactories = {
memcache: this.getMemcacheClient,
memory: this.getMemoryClient,
none: this.getNoneClient,
};
private readonly logger: Logger;
private readonly store: keyof CacheManager['storeFactories'];
private readonly connection: string;
private readonly errorHandler: OptionalOnError;
/**
* Creates a new CacheManager instance by reading from the `backend` config
* section, specifically the `.cache` key.
*
* @param config The loaded application configuration.
*/
static fromConfig(
config: Config,
options: CacheManagerOptions = {},
): CacheManager {
// If no `backend.cache` config is provided, instantiate the CacheManager
// with a "NoStore" cache client.
const store = config.getOptionalString('backend.cache.store') || 'none';
const connectionString =
config.getOptionalString('backend.cache.connection') || '';
const logger = (options.logger || getRootLogger()).child({
type: 'cacheManager',
});
return new CacheManager(store, connectionString, logger, options.onError);
}
private constructor(
store: string,
connectionString: string,
logger: Logger,
errorHandler: OptionalOnError,
) {
if (!this.storeFactories.hasOwnProperty(store)) {
throw new Error(`Unknown cache store: ${store}`);
}
this.logger = logger;
this.store = store as keyof CacheManager['storeFactories'];
this.connection = connectionString;
this.errorHandler = errorHandler;
}
/**
* Generates a PluginCacheManager for consumption by plugins.
*
* @param pluginId The plugin that the cache manager should be created for. Plugin names should be unique.
*/
forPlugin(pluginId: string): PluginCacheManager {
return {
getClient: (opts = {}): CacheClient => {
const concreteClient = this.getClientWithTtl(pluginId, opts.defaultTtl);
// Always provide an error handler to avoid killing the process.
concreteClient.on('error', (err: Error) => {
// In all cases, just log the error.
this.logger.error(err);
// Invoke any custom error handler if provided.
if (typeof this.errorHandler === 'function') {
this.errorHandler(err);
}
});
return new DefaultCacheClient({
client: concreteClient,
});
},
};
}
private getClientWithTtl(pluginId: string, ttl: number | undefined): Keyv {
return this.storeFactories[this.store].call(this, pluginId, ttl);
}
private getMemcacheClient(
pluginId: string,
defaultTtl: number | undefined,
): Keyv {
return new Keyv({
namespace: pluginId,
ttl: defaultTtl,
store: new KeyvMemcache(this.connection),
});
}
private getMemoryClient(
pluginId: string,
defaultTtl: number | undefined,
): Keyv {
return new Keyv({
namespace: pluginId,
ttl: defaultTtl,
});
}
private getNoneClient(pluginId: string): Keyv {
return new Keyv({
namespace: pluginId,
store: new NoStore(),
});
}
}
+41
View File
@@ -0,0 +1,41 @@
/*
* Copyright 2021 Spotify AB
*
* 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.
*/
/**
* Storage class compatible with Keyv which always results in a no-op. This is
* used when no cache store is configured in a Backstage backend instance.
*/
export class NoStore extends Map<string, any> {
clear(): void {
return;
}
delete(_key: string): boolean {
return false;
}
get(_key: string) {
return;
}
has(_key: string): boolean {
return false;
}
set(_key: string, _value: any): this {
return this;
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { CacheClient } from './CacheClient';
export { CacheManager } from './CacheManager';
export type { PluginCacheManager } from './types';
+56
View File
@@ -0,0 +1,56 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { Logger } from 'winston';
import { CacheClient } from './CacheClient';
type ClientOptions = {
/**
* An optional default TTL (in milliseconds) to be set when getting a client
* instance. If not provided, data will persist indefinitely by default (or
* can be configured per entry at set-time).
*/
defaultTtl?: number;
};
export type OptionalOnError = ((err: Error) => void) | undefined;
export type CacheManagerOptions = {
/**
* An optional logger for use by the PluginCacheManager.
*/
logger?: Logger;
/**
* An optional handler for connection errors emitted from the underlying data
* store.
*/
onError?: OptionalOnError;
};
/**
* The PluginCacheManager manages access to cache stores that Plugins get.
*/
export type PluginCacheManager = {
/**
* getClient provides backend plugins cache connections for itself.
*
* The purpose of this method is to allow plugins to get isolated data
* stores so that plugins are discouraged from cache-level integration
* and/or cache key collisions.
*/
getClient: (options?: ClientOptions) => CacheClient;
};
@@ -46,11 +46,11 @@ describe('database connection', () => {
).toBeTruthy();
});
it('tries to create a mysql connection as a passthrough', () => {
it('returns a mysql connection', () => {
expect(() =>
createDatabaseClient(
new ConfigReader({
client: 'mysql',
client: 'mysql2',
connection: {
host: '127.0.0.1',
user: 'foo',
@@ -59,7 +59,7 @@ describe('database connection', () => {
},
}),
),
).toThrowError(/Cannot find module 'mysql'/);
).toBeTruthy();
});
it('accepts overrides', () => {
@@ -14,9 +14,10 @@
* limitations under the License.
*/
import knexFactory, { Knex } from 'knex';
import { Config } from '@backstage/config';
import knexFactory, { Knex } from 'knex';
import { mergeDatabaseConfig } from './config';
import { createMysqlDatabaseClient, ensureMysqlDatabaseExists } from './mysql';
import { createPgDatabaseClient, ensurePgDatabaseExists } from './postgres';
import { createSqliteDatabaseClient } from './sqlite3';
@@ -36,6 +37,8 @@ export function createDatabaseClient(
if (client === 'pg') {
return createPgDatabaseClient(dbConfig, overrides);
} else if (client === 'mysql' || client === 'mysql2') {
return createMysqlDatabaseClient(dbConfig, overrides);
} else if (client === 'sqlite3') {
return createSqliteDatabaseClient(dbConfig, overrides);
}
@@ -60,6 +63,8 @@ export async function ensureDatabaseExists(
if (client === 'pg') {
return ensurePgDatabaseExists(dbConfig, ...databases);
} else if (client === 'mysql' || client === 'mysql2') {
return ensureMysqlDatabaseExists(dbConfig, ...databases);
}
return undefined;
@@ -0,0 +1,188 @@
/*
* Copyright 2020 Spotify AB
*
* 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 {
buildMysqlDatabaseConfig,
createMysqlDatabaseClient,
getMysqlConnectionConfig,
parseMysqlConnectionString,
} from './mysql';
describe('mysql', () => {
const createMockConnection = () => ({
host: 'acme',
user: 'foo',
password: 'bar',
database: 'foodb',
});
const createMockConnectionString = () => 'mysql://foo:bar@acme:3306/foodb';
const createConfig = (connection: any): Config =>
new ConfigReader({ client: 'mysql2', connection });
describe('buildMysqlDatabaseConfig', () => {
it('builds a mysql config', () => {
const mockConnection = createMockConnection();
expect(buildMysqlDatabaseConfig(createConfig(mockConnection))).toEqual({
client: 'mysql2',
connection: mockConnection,
useNullAsDefault: true,
});
});
it('builds a connection string config', () => {
const mockConnectionString = createMockConnectionString();
expect(
buildMysqlDatabaseConfig(createConfig(mockConnectionString)),
).toEqual({
client: 'mysql2',
connection: mockConnectionString,
useNullAsDefault: true,
});
});
it('overrides the database name', () => {
const mockConnection = createMockConnection();
expect(
buildMysqlDatabaseConfig(createConfig(mockConnection), {
connection: { database: 'other_db' },
}),
).toEqual({
client: 'mysql2',
connection: {
...mockConnection,
database: 'other_db',
},
useNullAsDefault: true,
});
});
it('adds additional config settings', () => {
const mockConnection = createMockConnection();
expect(
buildMysqlDatabaseConfig(createConfig(mockConnection), {
connection: { database: 'other_db' },
pool: { min: 0, max: 7 },
debug: true,
}),
).toEqual({
client: 'mysql2',
connection: {
...mockConnection,
database: 'other_db',
},
useNullAsDefault: true,
pool: { min: 0, max: 7 },
debug: true,
});
});
it('overrides the database from connection string', () => {
const mockConnectionString = createMockConnectionString();
const mockConnection = createMockConnection();
expect(
buildMysqlDatabaseConfig(createConfig(mockConnectionString), {
connection: { database: 'other_db' },
}),
).toEqual({
client: 'mysql2',
connection: {
...mockConnection,
port: 3306,
database: 'other_db',
},
useNullAsDefault: true,
});
});
});
describe('getMysqlConnectionConfig', () => {
it('returns the connection object back', () => {
const mockConnection = createMockConnection();
const config = createConfig(mockConnection);
expect(getMysqlConnectionConfig(config)).toEqual(mockConnection);
});
it('does not parse the connection string', () => {
const mockConnection = createMockConnection();
const config = createConfig(mockConnection);
expect(getMysqlConnectionConfig(config, true)).toEqual(mockConnection);
});
it('automatically parses the connection string', () => {
const mockConnection = createMockConnection();
const mockConnectionString = createMockConnectionString();
const config = createConfig(mockConnectionString);
expect(getMysqlConnectionConfig(config)).toEqual({
...mockConnection,
port: 3306,
});
});
it('parses the connection string', () => {
const mockConnection = createMockConnection();
const mockConnectionString = createMockConnectionString();
const config = createConfig(mockConnectionString);
expect(getMysqlConnectionConfig(config, true)).toEqual({
...mockConnection,
port: 3306,
});
});
});
describe('createMysqlDatabaseClient', () => {
it('creates a mysql knex instance', () => {
expect(
createMysqlDatabaseClient(
createConfig({
host: 'acme',
user: 'foo',
password: 'bar',
database: 'foodb',
}),
),
).toBeTruthy();
});
});
describe('parseMysqlConnectionString', () => {
it('parses a connection string uri', () => {
expect(
parseMysqlConnectionString(
'mysql://u:pass@foobar:3307/dbname?ssl=require',
),
).toEqual({
host: 'foobar',
user: 'u',
password: 'pass',
port: 3307,
database: 'dbname',
ssl: 'require',
});
});
});
});
@@ -0,0 +1,161 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Config } from '@backstage/config';
import { InputError } from '@backstage/errors';
import knexFactory, { Knex } from 'knex';
import { mergeDatabaseConfig } from './config';
import yn from 'yn';
/**
* Creates a knex mysql database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function createMysqlDatabaseClient(
dbConfig: Config,
overrides?: Knex.Config,
) {
const knexConfig = buildMysqlDatabaseConfig(dbConfig, overrides);
const database = knexFactory(knexConfig);
return database;
}
/**
* Builds a knex mysql database connection
*
* @param dbConfig The database config
* @param overrides Additional options to merge with the config
*/
export function buildMysqlDatabaseConfig(
dbConfig: Config,
overrides?: Knex.Config,
) {
return mergeDatabaseConfig(
dbConfig.get(),
{
connection: getMysqlConnectionConfig(dbConfig, !!overrides),
useNullAsDefault: true,
},
overrides,
);
}
/**
* Gets the mysql connection config
*
* @param dbConfig The database config
* @param parseConnectionString Flag to explicitly control connection string parsing
*/
export function getMysqlConnectionConfig(
dbConfig: Config,
parseConnectionString?: boolean,
): Knex.MySqlConnectionConfig | string {
const connection = dbConfig.get('connection') as any;
const isConnectionString =
typeof connection === 'string' || connection instanceof String;
const autoParse = typeof parseConnectionString !== 'boolean';
const shouldParseConnectionString = autoParse
? isConnectionString
: parseConnectionString && isConnectionString;
return shouldParseConnectionString
? parseMysqlConnectionString(connection as string)
: connection;
}
/**
* Parses a mysql connection string.
*
* e.g. mysql://examplename:somepassword@examplehost:3306/dbname
* @param connectionString The mysql connection string
*/
export function parseMysqlConnectionString(
connectionString: string,
): Knex.MySqlConnectionConfig {
try {
const {
protocol,
username,
password,
port,
hostname,
pathname,
searchParams,
} = new URL(connectionString);
if (protocol !== 'mysql:') {
throw new Error(`Unknown protocol ${protocol}`);
} else if (!username || !password) {
throw new Error(`Missing username/password`);
} else if (!pathname.match(/^\/[^/]+$/)) {
throw new Error(`Expected single path segment`);
}
const result: Knex.MySqlConnectionConfig = {
user: username,
password,
host: hostname,
port: Number(port || 3306),
database: decodeURIComponent(pathname.substr(1)),
};
const ssl = searchParams.get('ssl');
if (ssl) {
result.ssl = ssl;
}
const debug = searchParams.get('debug');
if (debug) {
result.debug = yn(debug);
}
return result;
} catch (e) {
throw new InputError(
`Error while parsing MySQL connection string, ${e}`,
e,
);
}
}
/**
* Creates the missing mysql database if it does not exist
*
* @param dbConfig The database config
* @param databases The names of the databases to create
*/
export async function ensureMysqlDatabaseExists(
dbConfig: Config,
...databases: Array<string>
) {
const admin = createMysqlDatabaseClient(dbConfig, {
connection: {
database: (null as unknown) as string,
},
});
try {
const ensureDatabase = async (database: string) => {
await admin.raw(`CREATE DATABASE IF NOT EXISTS ??`, [database]);
};
await Promise.all(databases.map(ensureDatabase));
} finally {
await admin.destroy();
}
}
+1
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export * from './cache';
export * from './config';
export * from './database';
export * from './discovery';
@@ -29,11 +29,11 @@ import path from 'path';
import { NotModifiedError } from '@backstage/errors';
import { getVoidLogger } from '../logging';
import { AzureUrlReader } from './AzureUrlReader';
import { ReadTreeResponseFactory } from './tree';
import { DefaultReadTreeResponseFactory } from './tree';
const logger = getVoidLogger();
const treeResponseFactory = ReadTreeResponseFactory.create({
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
});
@@ -27,9 +27,9 @@ import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory } from './tree';
import { stripFirstDirectoryFromPath } from './tree/util';
import {
ReadTreeResponseFactory,
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
@@ -28,9 +28,9 @@ import os from 'os';
import path from 'path';
import { NotModifiedError } from '@backstage/errors';
import { BitbucketUrlReader } from './BitbucketUrlReader';
import { ReadTreeResponseFactory } from './tree';
import { DefaultReadTreeResponseFactory } from './tree';
const treeResponseFactory = ReadTreeResponseFactory.create({
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
});
@@ -27,9 +27,9 @@ import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory } from './tree';
import { stripFirstDirectoryFromPath } from './tree/util';
import {
ReadTreeResponseFactory,
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
@@ -19,7 +19,7 @@ import { msw } from '@backstage/test-utils';
import { setupServer } from 'msw/node';
import { getVoidLogger } from '../logging';
import { FetchUrlReader } from './FetchUrlReader';
import { ReadTreeResponseFactory } from './tree';
import { DefaultReadTreeResponseFactory } from './tree';
describe('FetchUrlReader', () => {
const worker = setupServer();
@@ -45,7 +45,7 @@ describe('FetchUrlReader', () => {
},
}),
logger: getVoidLogger(),
treeResponseFactory: ReadTreeResponseFactory.create({
treeResponseFactory: DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
}),
});
@@ -35,9 +35,9 @@ import {
GhTreeResponse,
GithubUrlReader,
} from './GithubUrlReader';
import { ReadTreeResponseFactory } from './tree';
import { DefaultReadTreeResponseFactory } from './tree';
const treeResponseFactory = ReadTreeResponseFactory.create({
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
});
@@ -26,8 +26,8 @@ import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory } from './tree';
import {
ReadTreeResponseFactory,
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
@@ -24,7 +24,7 @@ import os from 'os';
import path from 'path';
import { getVoidLogger } from '../logging';
import { GitlabUrlReader } from './GitlabUrlReader';
import { ReadTreeResponseFactory } from './tree';
import { DefaultReadTreeResponseFactory } from './tree';
import { NotModifiedError, NotFoundError } from '@backstage/errors';
import {
GitLabIntegration,
@@ -33,7 +33,7 @@ import {
const logger = getVoidLogger();
const treeResponseFactory = ReadTreeResponseFactory.create({
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
});
@@ -190,11 +190,17 @@ describe('GitlabUrlReader', () => {
default_branch: 'main',
};
const branchGitlabApiResponse = {
commit: {
const commitsGitlabApiResponse = [
{
id: 'sha123abc',
},
};
];
const specificPathCommitsGitlabApiResponse = [
{
id: 'sha456def',
},
];
beforeEach(() => {
worker.use(
@@ -221,17 +227,29 @@ describe('GitlabUrlReader', () => {
),
),
rest.get(
'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/branches/main',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(branchGitlabApiResponse),
),
),
rest.get(
'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/branches/branchDoesNotExist',
(_, res, ctx) => res(ctx.status(404)),
'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/commits',
(req, res, ctx) => {
const refName = req.url.searchParams.get('ref_name');
if (refName === 'main') {
const filepath = req.url.searchParams.get('path');
if (filepath === 'testFilepath') {
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(specificPathCommitsGitlabApiResponse),
);
}
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(commitsGitlabApiResponse),
);
}
if (refName === 'branchDoesNotExist') {
return res(ctx.status(404));
}
return res();
},
),
rest.get(
'https://gitlab.mycompany.com/api/v4/projects/backstage%2Fmock',
@@ -243,13 +261,26 @@ describe('GitlabUrlReader', () => {
),
),
rest.get(
'https://gitlab.mycompany.com/api/v4/projects/backstage%2Fmock/repository/branches/main',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(branchGitlabApiResponse),
),
'https://gitlab.mycompany.com/api/v4/projects/backstage%2Fmock/repository/commits',
(req, res, ctx) => {
const refName = req.url.searchParams.get('ref_name');
if (refName === 'main') {
const filepath = req.url.searchParams.get('path');
if (filepath === 'testFilepath') {
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(specificPathCommitsGitlabApiResponse),
);
}
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(commitsGitlabApiResponse),
);
}
return res();
},
),
rest.get(
'https://gitlab.mycompany.com/api/v4/projects/backstage%2Fmock/repository/archive.zip?sha=main',
@@ -351,7 +382,7 @@ describe('GitlabUrlReader', () => {
).resolves.toBe('# Test\n');
});
it('throws a NotModifiedError when given a etag in options', async () => {
it('throws a NotModifiedError when given a etag in options matching last commit', async () => {
const fnGitlab = async () => {
await gitlabProcessor.readTree('https://gitlab.com/backstage/mock', {
etag: 'sha123abc',
@@ -371,6 +402,29 @@ describe('GitlabUrlReader', () => {
await expect(fnHostedGitlab).rejects.toThrow(NotModifiedError);
});
it('throws a NotModifiedError when given a etag in options matching last commit affecting specified filepath', async () => {
const fnGitlab = async () => {
await gitlabProcessor.readTree(
'https://gitlab.com/backstage/mock/blob/main/testFilepath',
{
etag: 'sha456def',
},
);
};
const fnHostedGitlab = async () => {
await hostedGitlabProcessor.readTree(
'https://gitlab.mycompany.com/backstage/mock/blob/main/testFilepath',
{
etag: 'sha456def',
},
);
};
await expect(fnGitlab).rejects.toThrow(NotModifiedError);
await expect(fnHostedGitlab).rejects.toThrow(NotModifiedError);
});
it('should not throw error when given an outdated etag in options', async () => {
const response = await gitlabProcessor.readTree(
'https://gitlab.com/backstage/mock/tree/main',
@@ -389,12 +443,12 @@ describe('GitlabUrlReader', () => {
});
it('should throw error on missing branch', async () => {
const fnGithub = async () => {
const fnGitlab = async () => {
await gitlabProcessor.readTree(
'https://gitlab.com/backstage/mock/tree/branchDoesNotExist',
);
};
await expect(fnGithub).rejects.toThrow(NotFoundError);
await expect(fnGitlab).rejects.toThrow(NotFoundError);
});
});
@@ -408,11 +462,11 @@ describe('GitlabUrlReader', () => {
default_branch: 'main',
};
const branchGitlabApiResponse = {
commit: {
const commitsGitlabApiResponse = [
{
id: 'sha123abc',
},
};
];
beforeEach(() => {
worker.use(
@@ -439,13 +493,18 @@ describe('GitlabUrlReader', () => {
),
),
rest.get(
'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/branches/main',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(branchGitlabApiResponse),
),
'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/commits',
(req, res, ctx) => {
const refName = req.url.searchParams.get('ref_name');
if (refName === 'main') {
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(commitsGitlabApiResponse),
);
}
return res();
},
),
);
});

Some files were not shown because too many files have changed in this diff Show More