Merge branch 'backstage:master' into master

This commit is contained in:
Hasan Ozdemir
2022-06-04 01:01:14 +02:00
committed by GitHub
100 changed files with 4042 additions and 2142 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Fix the public path configuration of the frontend app build so that a trailing `/` is always appended when needed.
+65
View File
@@ -0,0 +1,65 @@
---
'@backstage/plugin-catalog-backend-module-azure': patch
---
Add a new provider `AzureDevOpsEntityProvider` as replacement for `AzureDevOpsDiscoveryProcessor`.
In order to migrate from the `AzureDevOpsDiscoveryProcessor` you need to apply
the following changes:
**Before:**
```yaml
# app-config.yaml
catalog:
locations:
- type: azure-discovery
target: https://dev.azure.com/myorg/myproject/_git/service-*?path=/catalog-info.yaml
```
```ts
/* packages/backend/src/plugins/catalog.ts */
import { AzureDevOpsDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-azure';
const builder = await CatalogBuilder.create(env);
/** ... other processors ... */
builder.addProcessor(new AzureDevOpsDiscoveryProcessor(env.reader));
```
**After:**
```yaml
# app-config.yaml
catalog:
providers:
azureDevOps:
anyProviderId:
host: selfhostedazure.yourcompany.com # This is only really needed for on-premise user, defaults to dev.azure.com
organization: myorg # For on-premise this would be your Collection
project: myproject
repository: service-*
path: /catalog-info.yaml
```
```ts
/* packages/backend/src/plugins/catalog.ts */
import { AzureDevOpsEntityProvider } from '@backstage/plugin-catalog-backend-module-azure';
const builder = await CatalogBuilder.create(env);
/** ... other processors and/or providers ... */
builder.addEntityProvider(
AzureDevOpsEntityProvider.fromConfig(env.config, {
logger: env.logger,
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 30 },
timeout: { minutes: 3 },
}),
}),
);
```
Visit [https://backstage.io/docs/integrations/azure/discovery](https://backstage.io/docs/integrations/azure/discovery) for more details and options on configuration.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-pagerduty': minor
---
**Breaking**: Use identityApi to provide auth token for pagerduty API calls.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-common': patch
---
`@beta` exports now replaced with `@public` exports
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
Fix broken app-config in the example in the README
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-common': patch
---
Replaced all usages of `@backstage/search-common` with `@backstage/plugin-search-common`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': patch
---
ability to configure refresh interval on Kubernetes tab
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': minor
---
**BREAKING**: Server-to-server tokens that are authenticated by the `ServerTokenManager` now must have an `exp` claim that has not expired. Tokens where the `exp` claim is in the past or missing are considered invalid and will throw an error. This is a followup to the deprecation from the `1.2` release of Backstage where perpetual tokens were deprecated. Be sure to update any usage of the `getToken()` method to have it be called every time a token is needed. Do not store tokens for later use.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend': patch
---
`RouterOptions` and `createRouter` now marked as public exports
-1
View File
@@ -25,7 +25,6 @@
"@backstage/integration": "1.2.0",
"@backstage/integration-react": "1.1.0",
"@backstage/release-manifests": "0.0.3",
"@backstage/search-common": "0.3.4",
"@techdocs/cli": "1.1.1",
"techdocs-cli-embedded-app": "0.2.70",
"@backstage/techdocs-common": "0.11.15",
+5
View File
@@ -0,0 +1,5 @@
---
'@techdocs/cli': patch
---
Updated dependency `cypress` to `^10.0.0`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Updated dependency `passport` to `^0.6.0`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Updated dependency `minimatch` to `5.1.0`.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/core-components': patch
'@backstage/plugin-codescene': patch
'@backstage/plugin-sonarqube': patch
---
Updated dependency `rc-progress` to `3.3.3`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search': patch
---
Fixed a bug that could cause analytics events in other parts of Backstage to capture nonsensical values resembling search modal state under some circumstances.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-search-backend-module-elasticsearch': patch
---
Additional types now exported publicly:
- ElasticSearchAgentOptions
- ElasticSearchConcreteQuery
- ElasticSearchQueryTranslator
- ElasticSearchConnectionConstructor,
- ElasticSearchTransportConstructor,
- ElasticSearchNodeOptions,
- ElasticSearchOptions,
- ElasticSearchAuth,
+26
View File
@@ -0,0 +1,26 @@
---
'@backstage/plugin-org': patch
---
Added the ability to use an additional `filter` when fetching groups in `MyGroupsSidebarItem` component. Example:
```diff
// app/src/components/Root/Root.tsx
<SidebarPage>
<Sidebar>
//...
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
//...
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<MyGroupsSidebarItem
singularTitle="My Squad"
pluralTitle="My Squads"
icon={GroupIcon}
+ filter={{ 'spec.type': 'team' }}
/>
//...
</SidebarGroup>
</ Sidebar>
</SidebarPage>
```
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-backend': patch
---
In order to ensure a good, stable TechDocs user experience when running TechDocs with `techdocs.builder` set to `local`, the number of concurrent builds has been limited to 10. Any additional builds requested concurrently will be queued and handled as prior builds complete. In the unlikely event that you need to handle more concurrent builds, consider scaling out your TechDocs backend deployment or using the `external` option for `techdocs.builder`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
fix Sidebar Contexts deprecation message
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-node': patch
---
Replaced all `@beta` exports with `@public` exports
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Handle binary files and files that are too large during dry-run content upload.
+1
View File
@@ -154,3 +154,4 @@ _If you're using Backstage in your organization, please try to add your company
| [Zego](https://www.zego.com) | [Sean Kenny](mailto:sean.kenny@zego.com) | Single pane of glass for organisational and operational information for all services across our systems. |
| [Absa Group Limited](https://www.absa.africa/absaafrica/) | [Chris Kieser](mailto:chris.kieser@absa.africa) | Developer portal for all development needs - security, AWS, k8s, build and deployment pipelines and more |
| [Nutrien Ag Solutions](https://www.nutrienagsolutions.com.au) | [Jan Quijano](mailto:jan.quijano@nutrien.com.au) | Software Project Catalog |
| [Lendingkart](https://www.lendingkart.com/) | [Dinesh Rajpoot](mailto:dinesh.rajpoot@lendingkart.com) | Service catalog, Software templates to enforce best practices and tech insights to track mandates & migrations.
@@ -1,4 +1,4 @@
FROM alpine:3.15
FROM alpine:3.16
RUN apk add --update \
git \
@@ -1,7 +1,7 @@
ConfluenceCollator.ts reference
```ts
import { DocumentCollator } from '@backstage/search-common';
import { DocumentCollator } from '@backstage/plugin-search-common';
import fetch from 'cross-fetch';
export class ConfluenceCollator implements DocumentCollator {
@@ -3,7 +3,7 @@ ConfluenceResultListItem.tsx reference
```tsx
import React from 'react';
import { Link } from '@backstage/core-components';
import { IndexableDocument } from '@backstage/search-common';
import { IndexableDocument } from '@backstage/plugin-search-common';
import {
Divider,
ListItem,
+1 -1
View File
@@ -5,7 +5,7 @@
"license": "MIT",
"private": true,
"dependencies": {
"cypress": "^7.3.0",
"cypress": "^10.0.0",
"typescript": "^4.1.3"
}
}
+63 -43
View File
@@ -7,7 +7,7 @@
resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cypress/request@^2.88.5":
"@cypress/request@^2.88.10":
version "2.88.10"
resolved "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce"
integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==
@@ -49,10 +49,10 @@
resolved "https://registry.npmjs.org/@types/node/-/node-14.18.13.tgz#6ad4d9db59e6b3faf98dcfe4ca9d2aec84443277"
integrity sha512-Z6/KzgyWOga3pJNS42A+zayjhPbf2zM3hegRQaOPnLOzEi86VV++6FLDWgR1LGrVCRufP/ph2daa3tEa5br1zA==
"@types/sinonjs__fake-timers@^6.0.2":
version "6.0.4"
resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d"
integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==
"@types/sinonjs__fake-timers@8.1.1":
version "8.1.1"
resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"
integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==
"@types/sizzle@^2.3.2":
version "2.3.3"
@@ -150,6 +150,11 @@ balanced-match@^1.0.0:
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
@@ -180,6 +185,14 @@ buffer-crc32@~0.2.3:
resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer@^5.6.0:
version "5.7.1"
resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
cachedir@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
@@ -220,7 +233,7 @@ cli-cursor@^3.1.0:
dependencies:
restore-cursor "^3.1.0"
cli-table3@~0.6.0:
cli-table3@~0.6.1:
version "0.6.2"
resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a"
integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==
@@ -290,24 +303,25 @@ cross-spawn@^7.0.0:
shebang-command "^2.0.0"
which "^2.0.1"
cypress@^7.3.0:
version "7.7.0"
resolved "https://registry.npmjs.org/cypress/-/cypress-7.7.0.tgz#0839ae28e5520536f9667d6c9ae81496b3836e64"
integrity sha512-uYBYXNoI5ym0UxROwhQXWTi8JbUEjpC6l/bzoGZNxoKGsLrC1SDPgIDJMgLX/MeEdPL0UInXLDUWN/rSyZUCjQ==
cypress@^10.0.0:
version "10.0.2"
resolved "https://registry.npmjs.org/cypress/-/cypress-10.0.2.tgz#6aeac1923d534f9850d57dad9496f9ea74034a23"
integrity sha512-7+C4KHYBcfZrawss+Gt5PlS35rfc6ySc59JcHDVsIMm1E/J35dqE41UEXpdtwIq3549umCerNWnFADzqib4kcA==
dependencies:
"@cypress/request" "^2.88.5"
"@cypress/request" "^2.88.10"
"@cypress/xvfb" "^1.2.4"
"@types/node" "^14.14.31"
"@types/sinonjs__fake-timers" "^6.0.2"
"@types/sinonjs__fake-timers" "8.1.1"
"@types/sizzle" "^2.3.2"
arch "^2.2.0"
blob-util "^2.0.2"
bluebird "^3.7.2"
buffer "^5.6.0"
cachedir "^2.3.0"
chalk "^4.1.0"
check-more-types "^2.24.0"
cli-cursor "^3.1.0"
cli-table3 "~0.6.0"
cli-table3 "~0.6.1"
commander "^5.1.0"
common-tags "^1.8.0"
dayjs "^1.10.4"
@@ -326,15 +340,15 @@ cypress@^7.3.0:
listr2 "^3.8.3"
lodash "^4.17.21"
log-symbols "^4.0.0"
minimist "^1.2.5"
minimist "^1.2.6"
ospath "^1.2.2"
pretty-bytes "^5.6.0"
ramda "~0.27.1"
proxy-from-env "1.0.0"
request-progress "^3.0.0"
semver "^7.3.2"
supports-color "^8.1.1"
tmp "~0.2.1"
untildify "^4.0.0"
url "^0.11.0"
yauzl "^2.10.0"
dashdash@^1.12.0:
@@ -560,6 +574,11 @@ human-signals@^1.1.1:
resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
indent-string@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
@@ -714,6 +733,13 @@ log-update@^4.0.0:
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -743,7 +769,7 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.5:
minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
@@ -821,6 +847,11 @@ pretty-bytes@^5.6.0:
resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
proxy-from-env@1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==
psl@^1.1.28:
version "1.8.0"
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
@@ -834,11 +865,6 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
punycode@1.3.2:
version "1.3.2"
resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@@ -849,16 +875,6 @@ qs@~6.5.2:
resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
querystring@0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
ramda@~0.27.1:
version "0.27.2"
resolved "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1"
integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==
request-progress@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
@@ -903,6 +919,13 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
semver@^7.3.2:
version "7.3.7"
resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
dependencies:
lru-cache "^6.0.0"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -1036,9 +1059,9 @@ type-fest@^0.21.3:
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
typescript@^4.1.3:
version "4.6.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==
version "4.7.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4"
integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==
universalify@^2.0.0:
version "2.0.0"
@@ -1050,14 +1073,6 @@ untildify@^4.0.0:
resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
url@^0.11.0:
version "0.11.0"
resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
dependencies:
punycode "1.3.2"
querystring "0.2.0"
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
@@ -1102,6 +1117,11 @@ wrappy@1:
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
+5 -1
View File
@@ -33,10 +33,14 @@ const serviceEntityPage = (
<EntityLayout>
{/* other tabs... */}
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent />
<EntityKubernetesContent refreshIntervalMs={30000} />
</EntityLayout.Route>
```
**Notes:**
- The optional `refreshIntervalMs` property on the `EntityKubernetesContent` defines the interval in which the content automatically refreshes, if not set this will default to 10 seconds.
That's it! But now, we need the Kubernetes Backend plugin for the frontend to
work.
+77 -23
View File
@@ -6,25 +6,95 @@ sidebar_label: Discovery
description: Automatically discovering catalog entities from repositories in an Azure DevOps organization
---
The Azure DevOps integration has a special discovery processor for discovering
catalog entities within an Azure DevOps. The processor will crawl the Azure
The Azure DevOps integration has a special entity provider for discovering
catalog entities within an Azure DevOps. The provider will crawl your Azure
DevOps organization and register entities matching the configured path. This can
be useful as an alternative to static locations or manually adding things to the
catalog.
This guide explains how to install and configure the Azure DevOps Entity Provider (recommended) or the Azure DevOps Processor.
## Dependencies
### Code Search Feature
Azure discovery is driven by the Code Search feature in Azure DevOps, this may not be enabled by default. For Azure
DevOps Services you can confirm this by looking at the installed extensions in your Organization Settings. For Azure
DevOps Server you'll find this information in your Collection Settings.
If the Code Search extension is not listed then you can install it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=ms.vss-code-search&targetId=f9352dac-ba6e-434e-9241-a848a510ce3f&utm_source=vstsproduct&utm_medium=SearchExtStatus).
### Azure Integration
Setup [Azure integration](locations.md) with `host` and `token`. Host must be `dev.azure.com` for Cloud users, otherwise set this to your on-premise hostname.
## Installation
You will have to add the processors in the catalog initialization code of your
backend. They are not installed by default, therefore you have to add a
dependency to `@backstage/plugin-catalog-backend-module-azure` to your backend
package.
At your configuration, you add one or more provider configs:
```yaml
# app-config.yaml
catalog:
providers:
azureDevOps:
yourProviderId: # identifies your dataset / provider independent of config changes
organization: myorg
project: myproject
repository: service-* # this will match all repos starting with service-*
path: /catalog-info.yaml
anotherProviderId: # another identifier
organization: myorg
project: myproject
repository: '*' # this will match all repos starting with service-*
path: /src/*/catalog-info.yaml # this will search for files deep inside the /src folder
yetAotherProviderId: # guess, what? Another one :)
host: selfhostedazure.yourcompany.com
organization: myorg
project: myproject
```
The parameters available are:
- `host:` Leave empty for Cloud hosted, otherwise set to your self-hosted instance host.
- `organization:` Your Organization slug (or Collection for on-premise users). Required.
- `project:` Your project slug. Required.
- `repository:` The repository name. Wildcards are supported as show on the examples above. If not set, all repositories will be searched.
- `path:` Where to find catalog-info.yaml files. Defaults to /catalog-info.yaml.
_Note:_ the path parameter follows the same rules as the search on Azure DevOps
web interface. For more details visit the
[official search documentation](https://docs.microsoft.com/en-us/azure/devops/project/search/get-started-search?view=azure-devops).
As this provider is not one of the default providers, you will first need to install
the Azure catalog plugin:
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-azure
```
And then add the processors to your catalog builder:
Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`:
```diff
/* packages/backend/src/plugins/catalog.ts */
+import { AzureDevOpsEntityProvider } from '@backstage/plugin-catalog-backend-module-azure';
const builder = await CatalogBuilder.create(env);
/** ... other processors and/or providers ... */
+builder.addEntityProvider(
+ AzureDevOpsEntityProvider.fromConfig(env.config, {
+ logger: env.logger,
+ schedule: env.scheduler.createScheduledTaskRunner({
+ frequency: Duration.fromObject({ minutes: 30 }),
+ timeout: Duration.fromObject({ minutes: 3 }),
+ }),
+ }),
+);
```
## Alternative Processor
As an alternative to the entity provider `AzureDevOpsEntityProvider`, you can still use the `AzureDevopsDiscoveryProcessor`.
```diff
// In packages/backend/src/plugins/catalog.ts
@@ -37,12 +107,6 @@ And then add the processors to your catalog builder:
+ builder.addProcessor(AzureDevOpsDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }));
```
## Configuration
To use the discovery processor, you'll need a Azure integration
[set up](locations.md) with a `AZURE_TOKEN`. Then you can add a location target
to the catalog configuration:
```yaml
catalog:
locations:
@@ -70,13 +134,3 @@ When using a custom pattern, the target is composed of five parts:
- The path within each repository to find the catalog YAML file. This will
usually be `/catalog-info.yaml`, `/src/*/catalog-info.yaml` or a similar
variation for catalog files stored in the root directory of each repository.
_Note:_ the path parameter follows the same rules as the search on Azure DevOps
web interface. For more details visit the
[official search documentation](https://docs.microsoft.com/en-us/azure/devops/project/search/get-started-search?view=azure-devops).
Azure discovery is driven by the Code Search feature in Azure DevOps, this may not be enabled by default. For Azure
DevOps Services you can confirm this by looking at the installed extensions in your Organization Settings. For Azure
DevOps Server you'll find this information in your Collection Settings.
If the Code Search extension is not listed then you can install it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=ms.vss-code-search&targetId=f9352dac-ba6e-434e-9241-a848a510ce3f&utm_source=vstsproduct&utm_medium=SearchExtStatus).
+1 -1
View File
@@ -70,7 +70,7 @@
"fs-extra": "10.1.0",
"husky": "^8.0.0",
"lerna": "^4.0.0",
"lint-staged": "^12.2.0",
"lint-staged": "^13.0.0",
"minimist": "^1.2.5",
"prettier": "^2.2.1",
"semver": "^7.3.2",
+1 -1
View File
@@ -92,7 +92,7 @@
"@types/react-dom": "*",
"@types/zen-observable": "^0.8.0",
"cross-env": "^7.0.0",
"cypress": "^9.5.0",
"cypress": "^10.0.0",
"eslint-plugin-cypress": "^2.10.3",
"start-server-and-test": "^1.10.11"
},
@@ -171,7 +171,6 @@ describe('ServerTokenManager', () => {
it('should throw for expired tokens, and re-issue new ones', async () => {
jest.useFakeTimers();
const warn = jest.spyOn(logger, 'warn');
const tokenManager = ServerTokenManager.fromConfig(configWithSecret, {
logger,
@@ -179,14 +178,12 @@ describe('ServerTokenManager', () => {
const { token: token1 } = await tokenManager.getToken();
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
expect(warn.mock.calls.length).toBe(0);
// Right before the reissue timeout, it still returns the same token
jest.advanceTimersByTime(9 * 60 * 1000);
const { token: token1Again } = await tokenManager.getToken();
expect(token1).toEqual(token1Again);
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
expect(warn.mock.calls.length).toBe(0);
// Right after the reissue timeout, the old ones are still valid but returning a new token
jest.advanceTimersByTime(2 * 60 * 1000);
@@ -194,14 +191,47 @@ describe('ServerTokenManager', () => {
expect(token1).not.toEqual(token2);
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
await expect(tokenManager.authenticate(token2)).resolves.not.toThrow();
expect(warn.mock.calls.length).toBe(0);
// After expiry of the first one, it gets warnings but the newest one is still valid
jest.advanceTimersByTime(52 * 60 * 1000);
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
await expect(tokenManager.authenticate(token1)).rejects.toThrow(
'Invalid server token; caused by JWTExpired: "exp" claim timestamp check failed',
);
await expect(tokenManager.authenticate(token2)).resolves.not.toThrow();
expect(warn.mock.calls[0][0]).toMatchInlineSnapshot(
'"#### DEPRECATION WARNING: #### Server-to-server token had an expired exp claim, support for this has been deprecated and will result in errors in a future release"',
});
it('should work with a manually crafted JWT', async () => {
const secret = 'a1b2c3';
const token = await new jose.SignJWT({})
.setProtectedHeader({ alg: 'HS256' })
.setSubject('backstage-server')
.setExpirationTime(Date.now() + 1000 * 60 * 60)
.sign(jose.base64url.decode(secret));
const tokenManager = ServerTokenManager.fromConfig(
new ConfigReader({
backend: { auth: { keys: [{ secret }] } },
}),
{ logger },
);
await expect(tokenManager.authenticate(token)).resolves.toBeUndefined();
});
it('should reject tokens without exp claim', async () => {
const secret = 'a1b2c3';
const token = await new jose.SignJWT({})
.setProtectedHeader({ alg: 'HS256' })
.setSubject('backstage-server')
.sign(jose.base64url.decode(secret));
const tokenManager = ServerTokenManager.fromConfig(
new ConfigReader({
backend: { auth: { keys: [{ secret }] } },
}),
{ logger },
);
await expect(tokenManager.authenticate(token)).rejects.toThrow(
'Invalid server token; caused by AuthenticationError: Server-to-server token had no exp claim',
);
});
});
@@ -15,7 +15,7 @@
*/
import { Config } from '@backstage/config';
import { AuthenticationError, NotAllowedError } from '@backstage/errors';
import { AuthenticationError } from '@backstage/errors';
import { base64url, exportJWK, generateSecret, jwtVerify, SignJWT } from 'jose';
import { DateTime, Duration } from 'luxon';
import { Logger } from 'winston';
@@ -64,8 +64,6 @@ export class ServerTokenManager implements TokenManager {
private signingKey: Uint8Array;
private privateKeyPromise: Promise<void> | undefined;
private currentTokenPromise: Promise<{ token: string }> | undefined;
private warnedForMissingExpClaim = false;
private warnedForExpiredExpClaim = false;
/**
* Creates a token manager that issues static dummy tokens and never fails
@@ -184,36 +182,20 @@ export class ServerTokenManager implements TokenManager {
const {
protectedHeader: { alg },
payload: { sub, exp },
} = await jwtVerify(token, key, {
// TODO(freben): Holding on to tokens and reusing them is deprecated; remove this tolerance in a future release
clockTolerance: 3e9,
});
} = await jwtVerify(token, key);
if (alg !== TOKEN_ALG) {
throw new NotAllowedError(`Illegal alg "${alg}"`);
throw new AuthenticationError(`Illegal alg "${alg}"`);
}
if (sub !== TOKEN_SUB) {
throw new NotAllowedError(`Illegal sub "${sub}"`);
throw new AuthenticationError(`Illegal sub "${sub}"`);
}
// TODO(freben): Passing in tokens without an exp is deprecated; change this warning to an error in a future release
if (typeof exp !== 'number') {
if (!this.warnedForMissingExpClaim) {
this.warnedForMissingExpClaim = true;
this.options.logger.warn(
`#### DEPRECATION WARNING: #### Server-to-server token had no exp claim, support for this has been deprecated and will result in errors in a future release`,
);
}
}
// TODO(freben): Holding on to tokens and reusing them is deprecated; remove this tolerance in a future release
else if (exp * 1000 < Date.now()) {
if (!this.warnedForExpiredExpClaim) {
this.warnedForExpiredExpClaim = true;
this.options.logger.warn(
`#### DEPRECATION WARNING: #### Server-to-server token had an expired exp claim, support for this has been deprecated and will result in errors in a future release`,
);
}
throw new AuthenticationError(
'Server-to-server token had no exp claim',
);
}
return;
} catch (e) {
@@ -222,6 +204,6 @@ export class ServerTokenManager implements TokenManager {
}
}
throw new AuthenticationError(`Invalid server token: ${verifyError}`);
throw new AuthenticationError('Invalid server token', verifyError);
}
}
+1 -1
View File
@@ -94,7 +94,7 @@
"json-schema": "^0.4.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.4.2",
"minimatch": "5.0.1",
"minimatch": "5.1.0",
"node-fetch": "^2.6.7",
"node-libs-browser": "^2.2.1",
"npm-packlist": "^5.0.0",
+3 -2
View File
@@ -89,6 +89,7 @@ export async function createConfig(
const baseUrl = frontendConfig.getString('app.baseUrl');
const validBaseUrl = new URL(baseUrl);
const publicPath = validBaseUrl.pathname.replace(/\/$/, '');
if (checksEnabled) {
plugins.push(
new ForkTsCheckerWebpackPlugin({
@@ -121,7 +122,7 @@ export async function createConfig(
new HtmlWebpackPlugin({
template: paths.targetHtml,
templateParameters: {
publicPath: validBaseUrl.pathname.replace(/\/$/, ''),
publicPath,
config: frontendConfig,
},
}),
@@ -194,7 +195,7 @@ export async function createConfig(
},
output: {
path: paths.targetDist,
publicPath: validBaseUrl.pathname,
publicPath: `${publicPath}/`,
filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js',
chunkFilename: isDev
? '[name].chunk.js'
+1 -1
View File
@@ -56,7 +56,7 @@
"pluralize": "^8.0.0",
"prop-types": "^15.7.2",
"qs": "^6.9.4",
"rc-progress": "3.3.2",
"rc-progress": "3.3.3",
"react-helmet": "6.1.0",
"react-hook-form": "^7.12.2",
"react-markdown": "^8.0.0",
@@ -59,7 +59,7 @@ const defaultSidebarOpenStateContext = {
* Context whether the `Sidebar` is open
*
* @public @deprecated
* Use `<SidebarContextProvider>` + `useSidebar()` instead.
* Use `<SidebarOpenStateProvider>` + `useSidebarOpenState()` instead.
*/
export const LegacySidebarContext = createContext<SidebarContextType>(
defaultSidebarOpenStateContext,
@@ -38,7 +38,7 @@
"@types/node": "^16.11.26",
"@types/react-dom": "*",
"cross-env": "^7.0.0",
"cypress": "^9.5.0",
"cypress": "^10.0.0",
"eslint-plugin-cypress": "^2.10.3",
"start-server-and-test": "^1.10.11"
},
+1 -1
View File
@@ -45,7 +45,7 @@
"@types/node": "^16.11.26",
"@types/serve-handler": "^6.1.0",
"@types/webpack-env": "^1.15.3",
"cypress": "^9.5.0",
"cypress": "^10.0.0",
"cypress-plugin-snapshots": "^1.4.4",
"find-process": "^1.4.5",
"nodemon": "^2.0.2",
+1 -1
View File
@@ -61,7 +61,7 @@
"node-fetch": "^2.6.7",
"node-cache": "^5.1.2",
"openid-client": "^5.1.3",
"passport": "^0.5.2",
"passport": "^0.6.0",
"passport-bitbucket-oauth2": "^0.1.2",
"passport-github2": "^0.1.12",
"passport-gitlab2": "^5.0.0",
@@ -6,9 +6,12 @@
import { CatalogProcessor } from '@backstage/plugin-catalog-backend';
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend';
import { Config } from '@backstage/config';
import { EntityProvider } from '@backstage/plugin-catalog-backend';
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
import { LocationSpec } from '@backstage/plugin-catalog-backend';
import { Logger } from 'winston';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { TaskRunner } from '@backstage/backend-tasks';
// @public
export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor {
@@ -32,4 +35,22 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor {
emit: CatalogProcessorEmit,
): Promise<boolean>;
}
// @public
export class AzureDevOpsEntityProvider implements EntityProvider {
// (undocumented)
connect(connection: EntityProviderConnection): Promise<void>;
// (undocumented)
static fromConfig(
configRoot: Config,
options: {
logger: Logger;
schedule: TaskRunner;
},
): AzureDevOpsEntityProvider[];
// (undocumented)
getProviderName(): string;
// (undocumented)
refresh(logger: Logger): Promise<void>;
}
```
+59
View File
@@ -0,0 +1,59 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
interface AzureDevOpsConfig {
/**
* (Optional) The DevOps host; leave empty for `dev.azure.com`, otherwise set to your self-hosted instance host.
* @visibility backend
*/
host: string;
/**
* (Required) Your organization slug.
* @visibility backend
*/
organization: string;
/**
* (Required) Your project slug.
* @visibility backend
*/
project: string;
/**
* (Optional) The repository name. Wildcards are supported as show on the examples above.
* If not set, all repositories will be searched.
* @visibility backend
*/
repository?: string;
/**
* (Optional) Where to find catalog-info.yaml files. Wildcards are supported.
* If not set, defaults to /catalog-info.yaml.
* @visibility backend
*/
path?: string;
}
export interface Config {
catalog?: {
/**
* List of provider-specific options and attributes
*/
providers?: {
/**
* AzureDevopsEntityProvider configuration
*/
azureDevOps?: Record<string, AzureDevOpsConfig>;
};
};
}
@@ -35,6 +35,7 @@
"dependencies": {
"@backstage/backend-common": "^0.13.6-next.1",
"@backstage/catalog-model": "^1.0.3-next.0",
"@backstage/backend-tasks": "^0.3.2-next.0",
"@backstage/config": "^1.0.1",
"@backstage/errors": "^1.0.0",
"@backstage/integration": "^1.2.1-next.1",
@@ -42,6 +43,7 @@
"@backstage/types": "^1.0.0",
"lodash": "^4.17.21",
"msw": "^0.42.0",
"uuid": "^8.0.0",
"node-fetch": "^2.6.7",
"winston": "^3.2.1"
},
@@ -51,6 +53,8 @@
"@types/lodash": "^4.14.151"
},
"files": [
"dist"
]
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
@@ -20,4 +20,5 @@
* @packageDocumentation
*/
export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor';
export { AzureDevOpsDiscoveryProcessor } from './processors';
export { AzureDevOpsEntityProvider } from './providers';
@@ -21,9 +21,9 @@ import {
AzureDevOpsDiscoveryProcessor,
parseUrl,
} from './AzureDevOpsDiscoveryProcessor';
import { codeSearch } from './lib';
import { codeSearch } from '../lib';
jest.mock('./lib');
jest.mock('../lib');
const mockCodeSearch = codeSearch as jest.MockedFunction<typeof codeSearch>;
describe('AzureDevOpsDiscoveryProcessor', () => {
@@ -26,7 +26,7 @@ import {
processingResult,
} from '@backstage/plugin-catalog-backend';
import { Logger } from 'winston';
import { codeSearch } from './lib';
import { codeSearch } from '../lib';
/**
* Extracts repositories out of an Azure DevOps org.
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor';
@@ -0,0 +1,195 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks';
import { ConfigReader } from '@backstage/config';
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
import { CodeSearchResultItem } from '../lib';
import { AzureDevOpsEntityProvider } from './AzureDevOpsEntityProvider';
import { codeSearch } from '../lib';
jest.mock('../lib');
const mockCodeSearch = codeSearch as jest.MockedFunction<typeof codeSearch>;
class PersistingTaskRunner implements TaskRunner {
private tasks: TaskInvocationDefinition[] = [];
getTasks() {
return this.tasks;
}
run(task: TaskInvocationDefinition): Promise<void> {
this.tasks.push(task);
return Promise.resolve(undefined);
}
}
const logger = getVoidLogger();
describe('AzureDevOpsEntityProvider', () => {
afterEach(() => {
mockCodeSearch.mockClear();
});
const expectMutation = async (
providerId: string,
providerConfig: object,
codeSearchResults: CodeSearchResultItem[],
expectedBaseUrl: string,
names: Record<string, string>,
integrationConfig?: object,
) => {
const config = new ConfigReader({
integrations: {
azure: integrationConfig ? [integrationConfig] : [],
},
catalog: {
providers: {
azureDevOps: {
[providerId]: providerConfig,
},
},
},
});
mockCodeSearch.mockResolvedValueOnce(codeSearchResults);
const schedule = new PersistingTaskRunner();
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
};
const provider = AzureDevOpsEntityProvider.fromConfig(config, {
logger,
schedule,
})[0];
expect(provider.getProviderName()).toEqual(
`AzureDevOpsEntityProvider:${providerId}`,
);
await provider.connect(entityProviderConnection);
const taskDef = schedule.getTasks()[0];
expect(taskDef.id).toEqual(
`AzureDevOpsEntityProvider:${providerId}:refresh`,
);
await (taskDef.fn as () => Promise<void>)();
const expectedEntities = codeSearchResults.map(item => {
const url = encodeURI(
`${expectedBaseUrl}/_git/${item.repository.name}?path=${item.path}`,
);
return {
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
annotations: {
'backstage.io/managed-by-location': `url:${url}`,
'backstage.io/managed-by-origin-location': `url:${url}`,
},
name: names[`${item.repository.name}?path=${item.path}`],
},
spec: {
presence: 'required',
target: `${url}`,
type: 'url',
},
},
locationKey: `AzureDevOpsEntityProvider:${providerId}`,
};
});
expect(entityProviderConnection.applyMutation).toBeCalledWith({
type: 'full',
entities: expectedEntities,
});
};
// eslint-disable-next-line jest/expect-expect
it('no mutation when repos are empty', async () => {
return expectMutation(
'allRepos',
{
organization: 'myorganization',
project: 'myproject',
},
[],
'https://dev.azure.com/myorganization/myproject',
{},
);
});
// eslint-disable-next-line jest/expect-expect
it('single mutation when repos have 1 file found', async () => {
return expectMutation(
'allReposSingleFile',
{
organization: 'myorganization',
project: 'myproject',
},
[
{
fileName: 'catalog-info.yaml',
path: '/catalog-info.yaml',
repository: {
name: 'myrepo',
},
},
],
'https://dev.azure.com/myorganization/myproject',
{
'myrepo?path=/catalog-info.yaml':
'generated-87865246726bb12a8c4fb4f914443f1fbb91648c',
},
);
});
// eslint-disable-next-line jest/expect-expect
it('single mutation when multiple repos have multiple files', async () => {
return expectMutation(
'allReposMultipleFiles',
{
organization: 'myorganization',
project: 'myproject',
},
[
{
fileName: 'catalog-info.yaml',
path: '/catalog-info.yaml',
repository: {
name: 'myrepo',
},
},
{
fileName: 'catalog-info.yaml',
path: '/catalog-info.yaml',
repository: {
name: 'myotherrepo',
},
},
],
'https://dev.azure.com/myorganization/myproject',
{
'myrepo?path=/catalog-info.yaml':
'generated-87865246726bb12a8c4fb4f914443f1fbb91648c',
'myotherrepo?path=/catalog-info.yaml':
'generated-2deccac384c34d0dca37be0ebb4b1c8cf6913fe1',
},
);
});
});
@@ -0,0 +1,167 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TaskRunner } from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
import { AzureIntegration, ScmIntegrations } from '@backstage/integration';
import {
EntityProvider,
EntityProviderConnection,
LocationSpec,
locationSpecToLocationEntity,
} from '@backstage/plugin-catalog-backend';
import { readAzureDevOpsConfigs } from './config';
import { Logger } from 'winston';
import { AzureDevOpsConfig } from './types';
import * as uuid from 'uuid';
import { codeSearch, CodeSearchResultItem } from '../lib';
/**
* Provider which discovers catalog files within an Azure DevOps repositories.
*
* Use `AzureDevOpsEntityProvider.fromConfig(...)` to create instances.
*
* @public
*/
export class AzureDevOpsEntityProvider implements EntityProvider {
private readonly logger: Logger;
private readonly scheduleFn: () => Promise<void>;
private connection?: EntityProviderConnection;
static fromConfig(
configRoot: Config,
options: {
logger: Logger;
schedule: TaskRunner;
},
): AzureDevOpsEntityProvider[] {
const providerConfigs = readAzureDevOpsConfigs(configRoot);
return providerConfigs.map(providerConfig => {
const integration = ScmIntegrations.fromConfig(configRoot).azure.byHost(
providerConfig.host,
);
if (!integration) {
throw new Error(
`There is no Azure integration for host ${providerConfig.host}. Please add a configuration entry for it under integrations.azure`,
);
}
return new AzureDevOpsEntityProvider(
providerConfig,
integration,
options.logger,
options.schedule,
);
});
}
private constructor(
private readonly config: AzureDevOpsConfig,
private readonly integration: AzureIntegration,
logger: Logger,
schedule: TaskRunner,
) {
this.logger = logger.child({
target: this.getProviderName(),
});
this.scheduleFn = this.createScheduleFn(schedule);
}
private createScheduleFn(schedule: TaskRunner): () => Promise<void> {
return async () => {
const taskId = `${this.getProviderName()}:refresh`;
return schedule.run({
id: taskId,
fn: async () => {
const logger = this.logger.child({
class: AzureDevOpsEntityProvider.prototype.constructor.name,
taskId,
taskInstanceId: uuid.v4(),
});
try {
await this.refresh(logger);
} catch (error) {
logger.error(error);
}
},
});
};
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
getProviderName(): string {
return `AzureDevOpsEntityProvider:${this.config.id}`;
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection;
await this.scheduleFn();
}
async refresh(logger: Logger) {
if (!this.connection) {
throw new Error('Not initialized');
}
logger.info('Discovering Azure DevOps catalog files');
const files = await codeSearch(
this.integration.config,
this.config.organization,
this.config.project,
this.config.repository,
this.config.path,
);
logger.info(`Discovered ${files.length} catalog files`);
const locations = files.map(key => this.createLocationSpec(key));
await this.connection.applyMutation({
type: 'full',
entities: locations.map(location => {
return {
locationKey: this.getProviderName(),
entity: locationSpecToLocationEntity({ location }),
};
}),
});
logger.info(
`Committed ${locations.length} locations for AzureDevOps catalog files`,
);
}
private createLocationSpec(file: CodeSearchResultItem): LocationSpec {
return {
type: 'url',
target: this.createObjectUrl(file),
presence: 'required',
};
}
private createObjectUrl(file: CodeSearchResultItem): string {
const baseUrl = `https://${this.config.host}/${this.config.organization}/${this.config.project}`;
return encodeURI(
`${baseUrl}/_git/${file.repository.name}?path=${file.path}`,
);
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { readAzureDevOpsConfigs } from './config';
describe('readAzureDevOpsConfigs', () => {
it('reads all provider configs and set default values', () => {
const provider1 = {
host: 'azure.mycompany.com',
organization: 'mycompany',
project: 'myproject',
};
const provider2 = {
organization: 'mycompany',
project: 'myproject',
};
const provider3 = {
organization: 'mycompany',
project: 'myproject',
repository: 'service-*',
};
const config = {
catalog: {
providers: {
azureDevOps: { provider1, provider2, provider3 },
},
},
};
const actual = readAzureDevOpsConfigs(new ConfigReader(config));
expect(actual).toHaveLength(3);
expect(actual[0]).toEqual({
...provider1,
path: '/catalog-info.yaml',
repository: '*',
id: 'provider1',
});
expect(actual[1]).toEqual({
...provider2,
host: 'dev.azure.com',
path: '/catalog-info.yaml',
repository: '*',
id: 'provider2',
});
expect(actual[2]).toEqual({
...provider3,
host: 'dev.azure.com',
path: '/catalog-info.yaml',
id: 'provider3',
});
});
});
@@ -0,0 +1,53 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Config } from '@backstage/config';
import { AzureDevOpsConfig } from './types';
export function readAzureDevOpsConfigs(config: Config): AzureDevOpsConfig[] {
const configs: AzureDevOpsConfig[] = [];
const providerConfigs = config.getOptionalConfig(
'catalog.providers.azureDevOps',
);
if (!providerConfigs) {
return configs;
}
for (const id of providerConfigs.keys()) {
configs.push(readAzureDevOpsConfig(id, providerConfigs.getConfig(id)));
}
return configs;
}
function readAzureDevOpsConfig(id: string, config: Config): AzureDevOpsConfig {
const organization = config.getString('organization');
const project = config.getString('project');
const host = config.getOptionalString('host') || 'dev.azure.com';
const repository = config.getOptionalString('repository') || '*';
const path = config.getOptionalString('path') || '/catalog-info.yaml';
return {
id,
host,
organization,
project,
repository,
path,
};
}
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { AzureDevOpsEntityProvider } from './AzureDevOpsEntityProvider';
@@ -0,0 +1,24 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type AzureDevOpsConfig = {
id: string;
host: string;
organization: string;
project: string;
repository: string;
path: string;
};
+1 -1
View File
@@ -4,7 +4,7 @@
```ts
import { BasicPermission } from '@backstage/plugin-permission-common';
import { IndexableDocument } from '@backstage/search-common';
import { IndexableDocument } from '@backstage/plugin-search-common';
import { ResourcePermission } from '@backstage/plugin-permission-common';
// @alpha
+1 -1
View File
@@ -35,7 +35,7 @@
},
"dependencies": {
"@backstage/plugin-permission-common": "^0.6.2-next.0",
"@backstage/search-common": "^0.3.5-next.0"
"@backstage/plugin-search-common": "^0.3.5-next.0"
},
"devDependencies": {
"@backstage/cli": "^0.17.2-next.1"
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IndexableDocument } from '@backstage/search-common';
import { IndexableDocument } from '@backstage/plugin-search-common';
/**
* The Document format for an Entity in the Catalog for search
+1 -1
View File
@@ -30,7 +30,7 @@
"@material-ui/core": "^4.9.10",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"rc-progress": "3.3.2",
"rc-progress": "3.3.3",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^17.2.4"
},
+11 -10
View File
@@ -26,6 +26,8 @@ yarn add --cwd packages/app @backstage/plugin-cost-insights
2. Create a CostInsights client. Clients must implement the [CostInsightsApi](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts) interface. Create your own or [use a template](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/example/templates/CostInsightsClient.ts) to get started.
Tip: You can also use the `ExampleCostInsightsClient` from `@backstage/plugin-cost-insights` to see how the plugin looks with some mock data.
```ts
// path/to/CostInsightsClient.ts
import { CostInsightsApi } from '@backstage/plugin-cost-insights';
@@ -171,16 +173,15 @@ costInsights:
name: Some Other Cloud Product
icon: data
currencies:
metricA:
currencyA:
label: Currency A
unit: Unit A
currencyB:
label: Currency B
kind: CURRENCY_B
unit: Unit B
prefix: B
rate: 3.5
currencyA:
label: Currency A
unit: Unit A
currencyB:
label: Currency B
kind: CURRENCY_B
unit: Unit B
prefix: B
rate: 3.5
```
## Alerts
+10 -3
View File
@@ -112,7 +112,14 @@ export interface DeploymentResources {
// Warning: (ae-missing-release-tag) "EntityKubernetesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const EntityKubernetesContent: (_props: {}) => JSX.Element;
export const EntityKubernetesContent: (
props: EntityKubernetesContentProps,
) => JSX.Element;
// @public
export type EntityKubernetesContentProps = {
refreshIntervalMs?: number;
};
// Warning: (ae-forgotten-export) The symbol "ErrorPanelProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "ErrorPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -303,6 +310,7 @@ export class KubernetesBackendClient implements KubernetesApi {
// @public (undocumented)
export const KubernetesContent: ({
entity,
refreshIntervalMs,
}: KubernetesContentProps) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "KubernetesDrawerable" needs to be exported by the entry point index.d.ts
@@ -373,11 +381,10 @@ export const PodsTable: ({
extraColumns,
}: PodsTablesProps) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const Router: (_props: Props) => JSX.Element;
export const Router: (props: { refreshIntervalMs?: number }) => JSX.Element;
// Warning: (ae-missing-release-tag) "ServiceAccountKubernetesAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
+10 -4
View File
@@ -32,9 +32,7 @@ export const isKubernetesAvailable = (entity: Entity) =>
entity.metadata.annotations?.[KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION],
);
type Props = {};
export const Router = (_props: Props) => {
export const Router = (props: { refreshIntervalMs?: number }) => {
const { entity } = useEntity();
const kubernetesAnnotationValue =
@@ -49,7 +47,15 @@ export const Router = (_props: Props) => {
) {
return (
<Routes>
<Route path="/" element={<KubernetesContent entity={entity} />} />
<Route
path="/"
element={
<KubernetesContent
entity={entity}
refreshIntervalMs={props.refreshIntervalMs}
/>
}
/>
</Routes>
);
}
@@ -25,10 +25,20 @@ import EmptyStateImage from '../assets/emptystate.svg';
import { useKubernetesObjects } from '../hooks';
import { Content, Page, Progress } from '@backstage/core-components';
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
type KubernetesContentProps = {
entity: Entity;
refreshIntervalMs?: number;
children?: React.ReactNode;
};
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
const { kubernetesObjects, error } = useKubernetesObjects(entity);
export const KubernetesContent = ({
entity,
refreshIntervalMs,
}: KubernetesContentProps) => {
const { kubernetesObjects, error } = useKubernetesObjects(
entity,
refreshIntervalMs,
);
const clustersWithErrors =
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
+1
View File
@@ -25,6 +25,7 @@ export {
kubernetesPlugin as plugin,
EntityKubernetesContent,
} from './plugin';
export type { EntityKubernetesContentProps } from './plugin';
export { Router, isKubernetesAvailable } from './Router';
export * from './api';
export * from './kubernetes-auth-provider';
+15 -1
View File
@@ -76,7 +76,21 @@ export const kubernetesPlugin = createPlugin({
},
});
export const EntityKubernetesContent = kubernetesPlugin.provide(
/**
* Props of EntityKubernetesContent
*
* @public
*/
export type EntityKubernetesContentProps = {
/**
* Sets the refresh interval in milliseconds. The default value is 10000 (10 seconds)
*/
refreshIntervalMs?: number;
};
export const EntityKubernetesContent: (
props: EntityKubernetesContentProps,
) => JSX.Element = kubernetesPlugin.provide(
createRoutableExtension({
name: 'EntityKubernetesContent',
component: () => import('./Router').then(m => m.Router),
+1
View File
@@ -50,6 +50,7 @@ export const MyGroupsSidebarItem: (props: {
singularTitle: string;
pluralTitle: string;
icon: IconComponent;
filter?: Record<string, string | symbol | (string | symbol)[]>;
}) => JSX.Element | null;
// @public (undocumented)
@@ -196,4 +196,92 @@ describe('MyGroupsSidebarItem Test', () => {
expect(rendered.getByLabelText('My Squads')).toBeInTheDocument();
});
});
describe('When an additional filter is not provided', () => {
it('catalogApi.getEntities() should be called with the default filter', async () => {
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
};
const mockCatalogApi: Partial<CatalogApi> = {
getEntities: jest.fn(),
};
await renderInTestApp(
<TestApiProvider
apis={[
[identityApiRef, identityApi],
[catalogApiRef, mockCatalogApi],
]}
>
<MyGroupsSidebarItem
singularTitle="My Squad"
pluralTitle="My Squads"
icon={GroupIcon}
/>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(mockCatalogApi.getEntities).toHaveBeenCalledWith({
filter: [
{
kind: 'group',
'relations.hasMember': 'user:default/guest',
},
],
fields: ['metadata', 'kind'],
});
});
});
describe('When an additional filter is provided', () => {
it('catalogApi.getEntities() should be called with an additional filter item', async () => {
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
};
const mockCatalogApi: Partial<CatalogApi> = {
getEntities: jest.fn(),
};
await renderInTestApp(
<TestApiProvider
apis={[
[identityApiRef, identityApi],
[catalogApiRef, mockCatalogApi],
]}
>
<MyGroupsSidebarItem
singularTitle="My Squad"
pluralTitle="My Squads"
icon={GroupIcon}
filter={{ 'spec.type': 'team' }}
/>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(mockCatalogApi.getEntities).toHaveBeenCalledWith({
filter: [
{
kind: 'group',
'relations.hasMember': 'user:default/guest',
'spec.type': 'team',
},
],
fields: ['metadata', 'kind'],
});
});
});
});
@@ -45,8 +45,9 @@ export const MyGroupsSidebarItem = (props: {
singularTitle: string;
pluralTitle: string;
icon: IconComponent;
filter?: Record<string, string | symbol | (string | symbol)[]>;
}) => {
const { singularTitle, pluralTitle, icon } = props;
const { singularTitle, pluralTitle, icon, filter } = props;
const identityApi = useApi(identityApiRef);
const catalogApi: CatalogApi = useApi(catalogApiRef);
@@ -56,7 +57,13 @@ export const MyGroupsSidebarItem = (props: {
const profile = await identityApi.getBackstageIdentity();
const response = await catalogApi.getEntities({
filter: [{ kind: 'group', 'relations.hasMember': profile.userEntityRef }],
filter: [
{
kind: 'group',
'relations.hasMember': profile.userEntityRef,
...(filter ?? {}),
},
],
fields: ['metadata', 'kind'],
});
+2
View File
@@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api';
import { ConfigApi } from '@backstage/core-plugin-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { IdentityApi } from '@backstage/core-plugin-api';
import { ReactNode } from 'react';
// Warning: (ae-missing-release-tag) "EntityPagerDutyCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -45,6 +46,7 @@ export class PagerDutyClient implements PagerDutyApi {
static fromConfig(
configApi: ConfigApi,
discoveryApi: DiscoveryApi,
identityApi: IdentityApi,
): PagerDutyClient;
// Warning: (ae-forgotten-export) The symbol "ChangeEvent" needs to be exported by the entry point index.d.ts
//
+9 -1
View File
@@ -29,6 +29,7 @@ import {
createApiRef,
DiscoveryApi,
ConfigApi,
IdentityApi,
} from '@backstage/core-plugin-api';
export class UnauthorizedError extends Error {}
@@ -38,13 +39,18 @@ export const pagerDutyApiRef = createApiRef<PagerDutyApi>({
});
export class PagerDutyClient implements PagerDutyApi {
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) {
static fromConfig(
configApi: ConfigApi,
discoveryApi: DiscoveryApi,
identityApi: IdentityApi,
) {
const eventsBaseUrl: string =
configApi.getOptionalString('pagerDuty.eventsBaseUrl') ??
'https://events.pagerduty.com/v2';
return new PagerDutyClient({
eventsBaseUrl,
discoveryApi,
identityApi,
});
}
constructor(private readonly config: ClientApiConfig) {}
@@ -124,11 +130,13 @@ export class PagerDutyClient implements PagerDutyApi {
}
private async getByUrl<T>(url: string): Promise<T> {
const { token: idToken } = await this.config.identityApi.getCredentials();
const options = {
method: 'GET',
headers: {
Accept: 'application/vnd.pagerduty+json;version=2',
'Content-Type': 'application/json',
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
};
const response = await this.request(url, options);
+2 -1
View File
@@ -15,7 +15,7 @@
*/
import { Incident, ChangeEvent, OnCall, Service } from '../components/types';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
export type TriggerAlarmRequest = {
integrationKey: string;
@@ -74,6 +74,7 @@ export type OnCallsResponse = {
export type ClientApiConfig = {
eventsBaseUrl?: string;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
};
export type RequestOptions = {
+8 -3
View File
@@ -21,6 +21,7 @@ import {
discoveryApiRef,
configApiRef,
createComponentExtension,
identityApiRef,
} from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
@@ -32,9 +33,13 @@ export const pagerDutyPlugin = createPlugin({
apis: [
createApiFactory({
api: pagerDutyApiRef,
deps: { discoveryApi: discoveryApiRef, configApi: configApiRef },
factory: ({ configApi, discoveryApi }) =>
PagerDutyClient.fromConfig(configApi, discoveryApi),
deps: {
discoveryApi: discoveryApiRef,
configApi: configApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
PagerDutyClient.fromConfig(configApi, discoveryApi, identityApi),
}),
],
});
@@ -0,0 +1,44 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { base64EncodeContent } from './DryRunContext';
// eslint-disable-next-line no-restricted-imports
import { TextEncoder } from 'util';
window.TextEncoder = TextEncoder;
describe('base64EncodeContent', () => {
it('encodes text files', () => {
expect(base64EncodeContent('abc')).toBe('YWJj');
expect(base64EncodeContent('abc'.repeat(1000000))).toBe(
btoa('<file too large>'),
);
});
it('encodes binary files', () => {
expect(base64EncodeContent('\x00\x01\x02')).toBe('AAEC');
expect(base64EncodeContent('😅')).toBe('8J+YhQ==');
// Triggers chunking
expect(base64EncodeContent('😅'.repeat(18000))).toBe(
'8J+YhfCfmIXwn5iF8J+YhfCfmIXwn5iF'.repeat(3000),
);
// Triggers size check
expect(base64EncodeContent('😅'.repeat(1000000))).toBe(
btoa('<file too large>'),
);
});
});
@@ -29,6 +29,9 @@ import React, {
import { scaffolderApiRef } from '../../api';
import { ScaffolderDryRunResponse } from '../../types';
const MAX_CONTENT_SIZE = 256 * 1024;
const CHUNK_SIZE = 0x8000;
interface DryRunOptions {
templateContent: string;
values: JsonObject;
@@ -54,6 +57,27 @@ interface DryRunProviderProps {
children: ReactNode;
}
export function base64EncodeContent(content: string): string {
if (content.length > MAX_CONTENT_SIZE) {
return btoa('<file too large>');
}
try {
return btoa(content);
} catch {
const decoder = new TextEncoder();
const buffer = decoder.encode(content);
const chunks = new Array<string>();
for (let offset = 0; offset < buffer.length; offset += CHUNK_SIZE) {
chunks.push(
String.fromCharCode(...buffer.slice(offset, offset + CHUNK_SIZE)),
);
}
return btoa(chunks.join(''));
}
}
export function DryRunProvider(props: DryRunProviderProps) {
const scaffolderApi = useApi(scaffolderApiRef);
@@ -110,7 +134,7 @@ export function DryRunProvider(props: DryRunProviderProps) {
secrets: {},
directoryContents: options.files.map(file => ({
path: file.path,
base64Content: btoa(file.content),
base64Content: base64EncodeContent(file.content),
})),
});
@@ -15,17 +15,37 @@ import { Logger } from 'winston';
import { SearchEngine } from '@backstage/plugin-search-common';
import { SearchQuery } from '@backstage/plugin-search-common';
// Warning: (ae-missing-release-tag) "ElasticSearchClientOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-search-backend-module-elasticsearch" does not have an export "ElasticSearchEngine"
//
// @public (undocumented)
export interface ElasticSearchAgentOptions {
// (undocumented)
keepAlive?: boolean;
// (undocumented)
keepAliveMsecs?: number;
// (undocumented)
maxFreeSockets?: number;
// (undocumented)
maxSockets?: number;
}
// @public (undocumented)
export type ElasticSearchAuth =
| {
username: string;
password: string;
}
| {
apiKey:
| string
| {
id: string;
api_key: string;
};
};
// @public
export interface ElasticSearchClientOptions {
// Warning: (ae-forgotten-export) The symbol "ElasticSearchAgentOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
agent?: ElasticSearchAgentOptions | ((opts?: any) => unknown) | false;
// Warning: (ae-forgotten-export) The symbol "ElasticSearchAuth" needs to be exported by the entry point index.d.ts
//
// (undocumented)
auth?: ElasticSearchAuth;
// (undocumented)
@@ -36,8 +56,6 @@ export interface ElasticSearchClientOptions {
};
// (undocumented)
compression?: 'gzip';
// Warning: (ae-forgotten-export) The symbol "ElasticSearchConnectionConstructor" needs to be exported by the entry point index.d.ts
//
// (undocumented)
Connection?: ElasticSearchConnectionConstructor;
// (undocumented)
@@ -50,8 +68,6 @@ export interface ElasticSearchClientOptions {
maxRetries?: number;
// (undocumented)
name?: string | symbol;
// Warning: (ae-forgotten-export) The symbol "ElasticSearchNodeOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
node?:
| string
@@ -92,12 +108,35 @@ export interface ElasticSearchClientOptions {
ssl?: ConnectionOptions;
// (undocumented)
suggestCompression?: boolean;
// Warning: (ae-forgotten-export) The symbol "ElasticSearchTransportConstructor" needs to be exported by the entry point index.d.ts
//
// (undocumented)
Transport?: ElasticSearchTransportConstructor;
}
// @public
export type ElasticSearchConcreteQuery = {
documentTypes?: string[];
elasticSearchQuery: Object;
pageSize: number;
};
// @public (undocumented)
export interface ElasticSearchConnectionConstructor {
// (undocumented)
new (opts?: any): any;
// (undocumented)
roles: {
MASTER: string;
DATA: string;
INGEST: string;
ML: string;
};
// (undocumented)
statuses: {
ALIVE: string;
DEAD: string;
};
}
// @public (undocumented)
export type ElasticSearchHighlightConfig = {
fragmentDelimiter: string;
@@ -115,6 +154,41 @@ export type ElasticSearchHighlightOptions = {
};
// @public (undocumented)
export interface ElasticSearchNodeOptions {
// (undocumented)
agent?: ElasticSearchAgentOptions;
// (undocumented)
headers?: Record<string, any>;
// (undocumented)
id?: string;
// (undocumented)
roles?: {
master: boolean;
data: boolean;
ingest: boolean;
ml: boolean;
};
// (undocumented)
ssl?: ConnectionOptions;
// (undocumented)
url: URL;
}
// @public
export type ElasticSearchOptions = {
logger: Logger;
config: Config;
aliasPostfix?: string;
indexPrefix?: string;
};
// @public
export type ElasticSearchQueryTranslator = (
query: SearchQuery,
options?: ElasticSearchQueryTranslatorOptions,
) => ElasticSearchConcreteQuery;
// @public
export type ElasticSearchQueryTranslatorOptions = {
highlightOptions?: ElasticSearchHighlightConfig;
};
@@ -128,8 +202,6 @@ export class ElasticSearchSearchEngine implements SearchEngine {
logger: Logger,
highlightOptions?: ElasticSearchHighlightOptions,
);
// Warning: (ae-forgotten-export) The symbol "ElasticSearchOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
static fromConfig({
logger,
@@ -142,22 +214,16 @@ export class ElasticSearchSearchEngine implements SearchEngine {
newClient<T>(create: (options: ElasticSearchClientOptions) => T): T;
// (undocumented)
query(query: SearchQuery): Promise<IndexableResultSet>;
// Warning: (ae-forgotten-export) The symbol "ElasticSearchQueryTranslator" needs to be exported by the entry point index.d.ts
//
// (undocumented)
setTranslator(translator: ElasticSearchQueryTranslator): void;
// Warning: (ae-forgotten-export) The symbol "ConcreteElasticSearchQuery" needs to be exported by the entry point index.d.ts
//
// (undocumented)
protected translator(
query: SearchQuery,
options?: ElasticSearchQueryTranslatorOptions,
): ConcreteElasticSearchQuery;
): ElasticSearchConcreteQuery;
}
// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
constructor(options: ElasticSearchSearchEngineIndexerOptions);
// (undocumented)
@@ -170,9 +236,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
initialize(): Promise<void>;
}
// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngineIndexerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export type ElasticSearchSearchEngineIndexerOptions = {
type: string;
indexPrefix: string;
@@ -181,4 +245,17 @@ export type ElasticSearchSearchEngineIndexerOptions = {
logger: Logger;
elasticSearchClient: Client;
};
// @public (undocumented)
export interface ElasticSearchTransportConstructor {
// (undocumented)
new (opts?: any): any;
// (undocumented)
sniffReasons: {
SNIFF_ON_START: string;
SNIFF_INTERVAL: string;
SNIFF_ON_CONNECTION_FAULT: string;
DEFAULT: string;
};
}
```
@@ -18,10 +18,12 @@ import type { ConnectionOptions as TLSConnectionOptions } from 'tls';
/**
* Options used to configure the `@elastic/elasticsearch` client and
* are what will be passed as an argument to the
* {@link ElasticSearchEngine.newClient} method
* {@link ElasticSearchSearchEngine.newClient} method
*
* They are drawn from the `ClientOptions` class of `@elastic/elasticsearch`,
* but are maintained separately so that this interface is not coupled to
*
* @public
*/
export interface ElasticSearchClientOptions {
provider?: 'aws' | 'elastic';
@@ -65,6 +67,9 @@ export interface ElasticSearchClientOptions {
disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
}
/**
* @public
*/
export type ElasticSearchAuth =
| {
username: string;
@@ -79,6 +84,9 @@ export type ElasticSearchAuth =
};
};
/**
* @public
*/
export interface ElasticSearchNodeOptions {
url: URL;
id?: string;
@@ -93,6 +101,9 @@ export interface ElasticSearchNodeOptions {
};
}
/**
* @public
*/
export interface ElasticSearchAgentOptions {
keepAlive?: boolean;
keepAliveMsecs?: number;
@@ -100,6 +111,9 @@ export interface ElasticSearchAgentOptions {
maxFreeSockets?: number;
}
/**
* @public
*/
export interface ElasticSearchConnectionConstructor {
new (opts?: any): any;
statuses: {
@@ -113,7 +127,9 @@ export interface ElasticSearchConnectionConstructor {
ML: string;
};
}
/**
* @public
*/
export interface ElasticSearchTransportConstructor {
new (opts?: any): any;
sniffReasons: {
@@ -19,7 +19,7 @@ import { ConfigReader } from '@backstage/config';
import { Client, errors } from '@elastic/elasticsearch';
import Mock from '@elastic/elasticsearch-mock';
import {
ConcreteElasticSearchQuery,
ElasticSearchConcreteQuery,
decodePageCursor,
ElasticSearchSearchEngine,
encodePageCursor,
@@ -131,7 +131,7 @@ describe('ElasticSearchSearchEngine', () => {
types: ['indexName'],
term: 'testTerm',
filters: { kind: 'testKind' },
}) as ConcreteElasticSearchQuery;
}) as ElasticSearchConcreteQuery;
expect(actualTranslatedQuery).toMatchObject({
documentTypes: ['indexName'],
@@ -170,7 +170,7 @@ describe('ElasticSearchSearchEngine', () => {
types: ['indexName'],
term: 'testTerm',
pageCursor: 'MQ==',
}) as ConcreteElasticSearchQuery;
}) as ElasticSearchConcreteQuery;
expect(actualTranslatedQuery).toMatchObject({
documentTypes: ['indexName'],
@@ -210,7 +210,7 @@ describe('ElasticSearchSearchEngine', () => {
foo: 123,
bar: true,
},
}) as ConcreteElasticSearchQuery;
}) as ElasticSearchConcreteQuery;
expect(actualTranslatedQuery).toMatchObject({
documentTypes: ['indexName'],
@@ -266,7 +266,7 @@ describe('ElasticSearchSearchEngine', () => {
types: ['indexName'],
term: 'testTerm',
filters: { kind: ['testKind', 'kastTeint'] },
}) as ConcreteElasticSearchQuery;
}) as ElasticSearchConcreteQuery;
expect(actualTranslatedQuery).toMatchObject({
documentTypes: ['indexName'],
@@ -327,7 +327,7 @@ describe('ElasticSearchSearchEngine', () => {
fragmentDelimiter: ' ... ',
},
},
) as ConcreteElasticSearchQuery;
) as ElasticSearchConcreteQuery;
expect(actualTranslatedQuery).toMatchObject({
documentTypes: ['indexName'],
@@ -369,7 +369,7 @@ describe('ElasticSearchSearchEngine', () => {
types: ['indexName'],
term: 'testTerm',
filters: { kind: { a: 'b' } },
}) as ConcreteElasticSearchQuery;
}) as ElasticSearchConcreteQuery;
expect(actualTranslatedQuery).toThrow();
});
});
@@ -36,25 +36,38 @@ import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineInd
export type { ElasticSearchClientOptions };
export type ConcreteElasticSearchQuery = {
/**
* Search query that the elasticsearch engine understands.
* @public
*/
export type ElasticSearchConcreteQuery = {
documentTypes?: string[];
elasticSearchQuery: Object;
pageSize: number;
};
/**
* Options available for the Elasticsearch specific query translator.
* @public
*/
export type ElasticSearchQueryTranslatorOptions = {
highlightOptions?: ElasticSearchHighlightConfig;
};
type ElasticSearchQueryTranslator = (
/**
* Elasticsearch specific query translator.
* @public
*/
export type ElasticSearchQueryTranslator = (
query: SearchQuery,
options?: ElasticSearchQueryTranslatorOptions,
) => ConcreteElasticSearchQuery;
) => ElasticSearchConcreteQuery;
type ElasticSearchOptions = {
/**
* Options for instansiate ElasticSearchSearchEngine
* @public
*/
export type ElasticSearchOptions = {
logger: Logger;
config: Config;
aliasPostfix?: string;
@@ -161,7 +174,7 @@ export class ElasticSearchSearchEngine implements SearchEngine {
protected translator(
query: SearchQuery,
options?: ElasticSearchQueryTranslatorOptions,
): ConcreteElasticSearchQuery {
): ElasticSearchConcreteQuery {
const { term, filters = {}, types, pageCursor } = query;
const filter = Object.entries(filters)
@@ -20,6 +20,10 @@ import { Client } from '@elastic/elasticsearch';
import { Readable } from 'stream';
import { Logger } from 'winston';
/**
* Options for instansiate ElasticSearchSearchEngineIndexer
* @public
*/
export type ElasticSearchSearchEngineIndexerOptions = {
type: string;
indexPrefix: string;
@@ -35,6 +39,10 @@ function duration(startTimestamp: [number, number]): string {
return `${seconds.toFixed(1)}s`;
}
/**
* Elasticsearch specific search engine indexer.
* @public
*/
export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
private received: number = 0;
private processed: number = 0;
@@ -16,11 +16,20 @@
export { ElasticSearchSearchEngine } from './ElasticSearchSearchEngine';
export type {
ConcreteElasticSearchQuery,
ElasticSearchAgentOptions,
ElasticSearchConnectionConstructor,
ElasticSearchTransportConstructor,
ElasticSearchNodeOptions,
ElasticSearchAuth,
} from './ElasticSearchClientOptions';
export type {
ElasticSearchConcreteQuery,
ElasticSearchClientOptions,
ElasticSearchHighlightConfig,
ElasticSearchHighlightOptions,
ElasticSearchQueryTranslator,
ElasticSearchQueryTranslatorOptions,
ElasticSearchOptions,
} from './ElasticSearchSearchEngine';
export type {
ElasticSearchSearchEngineIndexer,
@@ -22,10 +22,18 @@
export { ElasticSearchSearchEngine } from './engines';
export type {
ElasticSearchAgentOptions,
ElasticSearchConcreteQuery,
ElasticSearchClientOptions,
ElasticSearchHighlightConfig,
ElasticSearchHighlightOptions,
ElasticSearchQueryTranslator,
ElasticSearchQueryTranslatorOptions,
ElasticSearchConnectionConstructor,
ElasticSearchTransportConstructor,
ElasticSearchNodeOptions,
ElasticSearchOptions,
ElasticSearchAuth,
ElasticSearchSearchEngineIndexer,
ElasticSearchSearchEngineIndexerOptions,
} from './engines';
+19 -24
View File
@@ -24,7 +24,7 @@ import { Transform } from 'stream';
import { UrlReader } from '@backstage/backend-common';
import { Writable } from 'stream';
// @beta
// @public
export abstract class BatchSearchEngineIndexer extends Writable {
constructor(options: BatchSearchEngineOptions);
abstract finalize(): Promise<void>;
@@ -32,19 +32,19 @@ export abstract class BatchSearchEngineIndexer extends Writable {
abstract initialize(): Promise<void>;
}
// @beta (undocumented)
// @public
export type BatchSearchEngineOptions = {
batchSize: number;
};
// @beta (undocumented)
// @public
export type ConcreteLunrQuery = {
lunrQueryBuilder: lunr_2.Index.QueryBuilder;
documentTypes?: string[];
pageSize: number;
};
// @beta
// @public
export abstract class DecoratorBase extends Transform {
constructor();
abstract decorate(
@@ -54,7 +54,7 @@ export abstract class DecoratorBase extends Transform {
abstract initialize(): Promise<void>;
}
// @beta (undocumented)
// @public
export class IndexBuilder {
constructor({ logger, searchEngine }: IndexBuilderOptions);
addCollator({ factory, schedule }: RegisterCollatorParameters): void;
@@ -62,22 +62,20 @@ export class IndexBuilder {
build(): Promise<{
scheduler: Scheduler;
}>;
// (undocumented)
getDocumentTypes(): Record<string, DocumentTypeInfo>;
// (undocumented)
getSearchEngine(): SearchEngine;
}
// @beta (undocumented)
// @public
export type IndexBuilderOptions = {
searchEngine: SearchEngine;
logger: Logger;
};
// @beta (undocumented)
// @public
export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery;
// @beta (undocumented)
// @public
export class LunrSearchEngine implements SearchEngine {
constructor({ logger }: { logger: Logger });
// (undocumented)
@@ -100,7 +98,7 @@ export class LunrSearchEngine implements SearchEngine {
protected translator: QueryTranslator;
}
// @beta (undocumented)
// @public
export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
constructor();
// (undocumented)
@@ -115,7 +113,7 @@ export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
initialize(): Promise<void>;
}
// @beta
// @public
export class NewlineDelimitedJsonCollatorFactory
implements DocumentCollatorFactory
{
@@ -131,7 +129,7 @@ export class NewlineDelimitedJsonCollatorFactory
readonly visibilityPermission: Permission | undefined;
}
// @beta (undocumented)
// @public
export type NewlineDelimitedJsonCollatorFactoryOptions = {
type: string;
searchPattern: string;
@@ -140,18 +138,18 @@ export type NewlineDelimitedJsonCollatorFactoryOptions = {
visibilityPermission?: Permission;
};
// @beta
// @public
export interface RegisterCollatorParameters {
factory: DocumentCollatorFactory;
schedule: TaskRunner;
}
// @beta
// @public
export interface RegisterDecoratorParameters {
factory: DocumentDecoratorFactory;
}
// @beta (undocumented)
// @public
export class Scheduler {
constructor({ logger }: { logger: Logger });
addToSchedule({ id, task, scheduledRunner }: ScheduleTaskParameters): void;
@@ -160,25 +158,22 @@ export class Scheduler {
}
// @public
export interface ScheduleTaskParameters {
// (undocumented)
export type ScheduleTaskParameters = {
id: string;
// (undocumented)
scheduledRunner: TaskRunner;
// (undocumented)
task: TaskFunction;
}
scheduledRunner: TaskRunner;
};
export { SearchEngine };
// @beta
// @public
export class TestPipeline {
execute(): Promise<TestPipelineResult>;
withDocuments(documents: IndexableDocument[]): TestPipeline;
static withSubject(subject: Readable | Transform | Writable): TestPipeline;
}
// @beta
// @public
export type TestPipelineResult = {
error: unknown;
documents: IndexableDocument[];
@@ -28,7 +28,8 @@ import {
} from './types';
/**
* @beta
* Used for adding collators, decorators and compile them into tasks which are added to a scheduler returned to the caller.
* @public
*/
export class IndexBuilder {
private collators: Record<string, RegisterCollatorParameters>;
@@ -45,10 +46,16 @@ export class IndexBuilder {
this.searchEngine = searchEngine;
}
/**
* Responsible for returning the registered search engine.
*/
getSearchEngine(): SearchEngine {
return this.searchEngine;
}
/**
* Responsible for returning the registered document types.
*/
getDocumentTypes(): Record<string, DocumentTypeInfo> {
return this.documentTypes;
}
+6 -4
View File
@@ -24,16 +24,18 @@ type TaskEnvelope = {
};
/**
* @public ScheduleTaskParameters
* ScheduleTaskParameters
* @public
*/
export interface ScheduleTaskParameters {
export type ScheduleTaskParameters = {
id: string;
task: TaskFunction;
scheduledRunner: TaskRunner;
}
};
/**
* @beta
* Scheduler responsible for all search tasks.
* @public
*/
export class Scheduler {
private logger: Logger;
@@ -23,7 +23,8 @@ import { Readable } from 'stream';
import { Logger } from 'winston';
/**
* @beta
* Options for instansiate NewlineDelimitedJsonCollatorFactory
* @public
*/
export type NewlineDelimitedJsonCollatorFactoryOptions = {
type: string;
@@ -60,15 +61,13 @@ export type NewlineDelimitedJsonCollatorFactoryOptions = {
* });
* ```
*
* @beta
* @public
*/
export class NewlineDelimitedJsonCollatorFactory
implements DocumentCollatorFactory
{
/** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory."type"} */
readonly type: string;
/** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory.visibilityPermission} */
public readonly visibilityPermission: Permission | undefined;
private constructor(
@@ -123,7 +122,6 @@ export class NewlineDelimitedJsonCollatorFactory
}
}
/** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory.getCollator} */
async getCollator(): Promise<Readable> {
// Search for files matching the given pattern.
const lastUrl = await this.lastUrl();
@@ -27,7 +27,8 @@ import { Logger } from 'winston';
import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer';
/**
* @beta
* Type of translated query for the Lunr Search Engine.
* @public
*/
export type ConcreteLunrQuery = {
lunrQueryBuilder: lunr.Index.QueryBuilder;
@@ -41,12 +42,14 @@ type LunrResultEnvelope = {
};
/**
* @beta
* Translator repsonsible for translating search term and filters to a query that the Lunr Search Engine understands.
* @public
*/
export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery;
/**
* @beta
* Lunr specific search engine implementation.
* @public
*/
export class LunrSearchEngine implements SearchEngine {
protected lunrIndices: Record<string, lunr.Index> = {};
@@ -19,7 +19,8 @@ import lunr from 'lunr';
import { BatchSearchEngineIndexer } from '../indexing';
/**
* @beta
* Lunr specific search engine indexer
* @public
*/
export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
private schemaInitialized = false;
@@ -19,7 +19,8 @@ import { IndexableDocument } from '@backstage/plugin-search-common';
import { Writable } from 'stream';
/**
* @beta
* Options for {@link BatchSearchEngineIndexer}
* @public
*/
export type BatchSearchEngineOptions = {
batchSize: number;
@@ -28,7 +29,7 @@ export type BatchSearchEngineOptions = {
/**
* Base class encapsulating batch-based stream processing. Useful as a base
* class for search engine indexers.
* @beta
* @public
*/
export abstract class BatchSearchEngineIndexer extends Writable {
private batchSize: number;
@@ -21,7 +21,7 @@ import { Transform } from 'stream';
/**
* Base class encapsulating simple async transformations. Useful as a base
* class for Backstage search decorators.
* @beta
* @public
*/
export abstract class DecoratorBase extends Transform {
private initialized: Promise<undefined | Error>;
@@ -19,7 +19,7 @@ import { pipeline, Readable, Transform, Writable } from 'stream';
/**
* Object resolved after a test pipeline is executed.
* @beta
* @public
*/
export type TestPipelineResult = {
/**
@@ -37,7 +37,7 @@ export type TestPipelineResult = {
/**
* Test utility for Backstage Search collators, decorators, and indexers.
* @beta
* @public
*/
export class TestPipeline {
private collator?: Readable;
+4 -3
View File
@@ -23,7 +23,8 @@ import {
import { Logger } from 'winston';
/**
* @beta
* Options required to instantiate the index builder.
* @public
*/
export type IndexBuilderOptions = {
searchEngine: SearchEngine;
@@ -32,7 +33,7 @@ export type IndexBuilderOptions = {
/**
* Parameters required to register a collator.
* @beta
* @public
*/
export interface RegisterCollatorParameters {
/**
@@ -48,7 +49,7 @@ export interface RegisterCollatorParameters {
/**
* Parameters required to register a decorator
* @beta
* @public
*/
export interface RegisterDecoratorParameters {
/**
-4
View File
@@ -11,13 +11,9 @@ import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { SearchEngine } from '@backstage/plugin-search-backend-node';
// Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type RouterOptions = {
engine: SearchEngine;
@@ -51,6 +51,9 @@ const jsonObjectSchema: z.ZodSchema<JsonObject> = z.lazy(() => {
return z.record(jsonValueSchema);
});
/**
* @public
*/
export type RouterOptions = {
engine: SearchEngine;
types: Record<string, DocumentTypeInfo>;
@@ -61,6 +64,9 @@ export type RouterOptions = {
const allowedLocationProtocols = ['http:', 'https:'];
/**
* @public
*/
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
+16 -16
View File
@@ -11,46 +11,46 @@ import { Readable } from 'stream';
import { Transform } from 'stream';
import { Writable } from 'stream';
// @beta
// @public
export interface DocumentCollatorFactory {
getCollator(): Promise<Readable>;
readonly type: string;
readonly visibilityPermission?: Permission;
}
// @beta
// @public
export interface DocumentDecoratorFactory {
getDecorator(): Promise<Transform>;
readonly types?: string[];
}
// @beta
// @public
export type DocumentTypeInfo = {
visibilityPermission?: Permission;
};
// @beta
// @public
export type IndexableDocument = SearchDocument & {
authorization?: {
resourceRef: string;
};
};
// @beta (undocumented)
// @public (undocumented)
export type IndexableResult = Result<IndexableDocument>;
// @beta (undocumented)
// @public (undocumented)
export type IndexableResultSet = ResultSet<IndexableDocument>;
// @beta
// @public
export type QueryRequestOptions = {
token?: string;
};
// @beta
// @public
export type QueryTranslator = (query: SearchQuery) => unknown;
// @beta (undocumented)
// @public (undocumented)
export interface Result<TDocument extends SearchDocument> {
// (undocumented)
document: TDocument;
@@ -60,7 +60,7 @@ export interface Result<TDocument extends SearchDocument> {
type: string;
}
// @beta
// @public
export interface ResultHighlight {
// (undocumented)
fields: {
@@ -70,7 +70,7 @@ export interface ResultHighlight {
preTag: string;
}
// @beta (undocumented)
// @public (undocumented)
export interface ResultSet<TDocument extends SearchDocument> {
// (undocumented)
nextPageCursor?: string;
@@ -80,14 +80,14 @@ export interface ResultSet<TDocument extends SearchDocument> {
results: Result<TDocument>[];
}
// @beta
// @public
export interface SearchDocument {
location: string;
text: string;
title: string;
}
// @beta
// @public
export interface SearchEngine {
getIndexer(type: string): Promise<Writable>;
query(
@@ -97,7 +97,7 @@ export interface SearchEngine {
setTranslator(translator: QueryTranslator): void;
}
// @beta (undocumented)
// @public (undocumented)
export interface SearchQuery {
// (undocumented)
filters?: JsonObject;
@@ -109,9 +109,9 @@ export interface SearchQuery {
types?: string[];
}
// @beta (undocumented)
// @public (undocumented)
export type SearchResult = Result<SearchDocument>;
// @beta (undocumented)
// @public (undocumented)
export type SearchResultSet = ResultSet<SearchDocument>;
```
+16 -16
View File
@@ -19,7 +19,7 @@ import { JsonObject } from '@backstage/types';
import { Readable, Transform, Writable } from 'stream';
/**
* @beta
* @public
*/
export interface SearchQuery {
term: string;
@@ -29,10 +29,10 @@ export interface SearchQuery {
}
/**
* @beta
* Metadata for result relevant document fields with matched terms highlighted
* via wrapping in associated pre/post tags. The UI is expected to parse these
* field excerpts by replacing wrapping tags with applicable UI elements for rendering.
* @public
*/
export interface ResultHighlight {
/**
@@ -53,7 +53,7 @@ export interface ResultHighlight {
}
/**
* @beta
* @public
*/
export interface Result<TDocument extends SearchDocument> {
type: string;
@@ -62,7 +62,7 @@ export interface Result<TDocument extends SearchDocument> {
}
/**
* @beta
* @public
*/
export interface ResultSet<TDocument extends SearchDocument> {
results: Result<TDocument>[];
@@ -71,28 +71,28 @@ export interface ResultSet<TDocument extends SearchDocument> {
}
/**
* @beta
* @public
*/
export type SearchResult = Result<SearchDocument>;
/**
* @beta
* @public
*/
export type SearchResultSet = ResultSet<SearchDocument>;
/**
* @beta
* @public
*/
export type IndexableResult = Result<IndexableDocument>;
/**
* @beta
* @public
*/
export type IndexableResultSet = ResultSet<IndexableDocument>;
/**
* Base properties that all search documents must include.
* @beta
* @public
*/
export interface SearchDocument {
/**
@@ -117,7 +117,7 @@ export interface SearchDocument {
* backends working directly with documents being inserted or retrieved from
* search indexes. When dealing with documents in the frontend, use
* {@link SearchDocument}.
* @beta
* @public
*/
export type IndexableDocument = SearchDocument & {
/**
@@ -136,7 +136,7 @@ export type IndexableDocument = SearchDocument & {
* Information about a specific document type. Intended to be used in the
* {@link @backstage/plugin-search-backend-node#IndexBuilder} to collect information
* about the types stored in the index.
* @beta
* @public
*/
export type DocumentTypeInfo = {
/**
@@ -148,7 +148,7 @@ export type DocumentTypeInfo = {
/**
* Factory class for instantiating collators.
* @beta
* @public
*/
export interface DocumentCollatorFactory {
/**
@@ -171,7 +171,7 @@ export interface DocumentCollatorFactory {
/**
* Factory class for instantiating decorators.
* @beta
* @public
*/
export interface DocumentDecoratorFactory {
/**
@@ -190,13 +190,13 @@ export interface DocumentDecoratorFactory {
/**
* A type of function responsible for translating an abstract search query into
* a concrete query relevant to a particular search engine.
* @beta
* @public
*/
export type QueryTranslator = (query: SearchQuery) => unknown;
/**
* Options when querying a search engine.
* @beta
* @public
*/
export type QueryRequestOptions = {
token?: string;
@@ -206,7 +206,7 @@ export type QueryRequestOptions = {
* Interface that must be implemented by specific search engines, responsible
* for performing indexing and querying and translating abstract queries into
* concrete, search engine-specific queries.
* @beta
* @public
*/
export interface SearchEngine {
/**
@@ -37,7 +37,7 @@ export type SearchModalValue = {
const SearchModalContext = createVersionedContext<{
1: SearchModalValue | undefined;
}>('analytics-context');
}>('search-modal-context');
/**
* Props for the SearchModalProvider.
+1 -1
View File
@@ -46,7 +46,7 @@
"@material-ui/lab": "4.0.0-alpha.57",
"@material-ui/styles": "^4.10.0",
"cross-fetch": "^3.1.5",
"rc-progress": "3.3.2",
"rc-progress": "3.3.3",
"react-use": "^17.2.4"
},
"peerDependencies": {
@@ -159,6 +159,37 @@ describe('DocsSynchronizer', () => {
expect(DocsBuilder.prototype.build).toBeCalledTimes(1);
});
it('should limit concurrent updates', async () => {
// Given a build implementation that runs long...
MockedDocsBuilder.prototype.build.mockImplementation(
() => new Promise(() => {}),
);
(shouldCheckForUpdate as jest.Mock).mockReturnValue(true);
const entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
uid: '0',
name: 'test',
namespace: 'default',
},
};
// When more than 10 syncs are attempted...
for (let i = 0; i < 12; i++) {
docsSynchronizer.doSync({
responseHandler: mockResponseHandler,
entity,
preparers,
generators,
});
}
// Then still only 10 builds should have been triggered.
await new Promise<void>(resolve => resolve());
expect(DocsBuilder.prototype.build).toHaveBeenCalledTimes(10);
});
it('should not check for an update too often', async () => {
(shouldCheckForUpdate as jest.Mock).mockReturnValue(false);
@@ -25,6 +25,7 @@ import {
PublisherBase,
} from '@backstage/plugin-techdocs-node';
import fetch from 'node-fetch';
import pLimit, { Limit } from 'p-limit';
import { PassThrough } from 'stream';
import * as winston from 'winston';
import { TechDocsCache } from '../cache';
@@ -47,6 +48,7 @@ export class DocsSynchronizer {
private readonly config: Config;
private readonly scmIntegrations: ScmIntegrationRegistry;
private readonly cache: TechDocsCache | undefined;
private readonly buildLimiter: Limit;
constructor({
publisher,
@@ -69,6 +71,9 @@ export class DocsSynchronizer {
this.publisher = publisher;
this.scmIntegrations = scmIntegrations;
this.cache = cache;
// Single host/process: limit concurrent builds up to 10 at a time.
this.buildLimiter = pLimit(10);
}
async doSync({
@@ -123,7 +128,7 @@ export class DocsSynchronizer {
cache: this.cache,
});
const updated = await docsBuilder.build();
const updated = await this.buildLimiter(() => docsBuilder.build());
if (!updated) {
finish({ updated: false });
+13 -1
View File
@@ -40,7 +40,7 @@ import {
TSDocTagSyntaxKind,
} from '@microsoft/tsdoc';
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
import { ApiPackage, ApiModel } from '@microsoft/api-extractor-model';
import { ApiPackage, ApiModel, ApiItem } from '@microsoft/api-extractor-model';
import {
IMarkdownDocumenterOptions,
MarkdownDocumenter,
@@ -256,7 +256,9 @@ const NO_WARNING_PACKAGES = [
'plugins/scaffolder-backend-module-rails',
'plugins/scaffolder-backend-module-yeoman',
'plugins/scaffolder-common',
'plugins/search-backend',
'plugins/search-backend-node',
'plugins/search-backend-module-elasticsearch',
'plugins/search-common',
'plugins/search-react',
'plugins/techdocs',
@@ -939,6 +941,16 @@ async function buildDocs({
this._markdownEmitter = new CustomCustomMarkdownEmitter(newModel);
}
private _getFilenameForApiItem(apiItem: ApiItem): string {
const filename: string = super._getFilenameForApiItem(apiItem);
if (filename.includes('.html.')) {
return filename.replace(/\.html\./g, '._html.');
}
return filename;
}
// We don't really get many chances to modify the generated AST
// so we hook in wherever we can. In this case we add the front matter
// just before writing the breadcrumbs at the top.
+1928 -1567
View File
File diff suppressed because it is too large Load Diff
+517 -294
View File
File diff suppressed because it is too large Load Diff