This commit is contained in:
Isaiah Thiessen
2022-06-03 13:27:56 -07:00
153 changed files with 5677 additions and 2290 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Fixes review mask in `MultistepJsonForm` to work as documented. `show: true` no longer needed when mask is set.
+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`
+23
View File
@@ -0,0 +1,23 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
Fixed a bug in `publish:github` action that didn't permit to add users as collaborators.
This fix required changing the way parameters are passed to the action.
In order to add a team as collaborator, now you must use the `team` field instead of `username`.
In order to add a user as collaborator, you must use the `user` field.
It's still possible to use the field `username` but is deprecated in favor of `team`.
```yaml
- id: publish
name: Publish
action: publish:github
input:
repoUrl: ...
collaborators:
- access: ...
team: my_team
- access: ...
user: my_username
```
+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
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
- Added an optional `list` method on the `TaskBroker` and `TaskStore` interface to list tasks by an optional `userEntityRef`
- Implemented a `list` method on the `DatabaseTaskStore` class to list tasks by an optional `userEntityRef`
- Added a route under `/v2/tasks` to list tasks by a `userEntityRef` using the `createdBy` query parameter
-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 @@
---
'@backstage/cli': patch
---
Updated dependency `@rollup/plugin-commonjs` to `^22.0.0`.
+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`.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-scaffolder': minor
---
- Added a new page under `/create/tasks` to show tasks that have been run by the Scaffolder.
- Ability to filter these tasks by the signed in user, and all tasks.
- Added optional method to the `ScaffolderApi` interface called `listTasks` to get tasks with an required `filterByOwnership` parameter.
+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,
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Updated to support deployments of Azure DevOps Server under TFS or similar sub path
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/create-app': patch
---
Bump version of `cypress` in newly scaffolded Backstage Applications. To apply this change to your own instance, please make the following change to `packages/app/package.json` under `devDependencies`.
```diff
- "cypress": "^7.3.0",
+ "cypress": "^9.7.0",
```
+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 -1
View File
@@ -56,4 +56,4 @@ jobs:
run: yarn e2e-test run
env:
DEBUG: zombie
CYPRESS_VERIFY_TIMEOUT: 60000
CYPRESS_VERIFY_TIMEOUT: 600000
+33 -12
View File
@@ -21,7 +21,7 @@ _If you're using Backstage in your organization, please try to add your company
| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. |
| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. |
| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. |
| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit |
| [Expedia Group](https://www.expediagroup.com) | [@guillermomanzo](https://github.com/guillermomanzo), [Sheena Sharma](mailto:shesharma@expediagroup.com) | EG Common Developer Toolkit |
| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go |
| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling |
| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. |
@@ -122,15 +122,36 @@ _If you're using Backstage in your organization, please try to add your company
| [World Fuel Services](https://www.wfscorp.com/) | [Anirudh Kurapathi](https://github.com/anirudhkurapati), [Alex Kwon](https://github.com/alexkwon), [Lester Hernandez](https://github.com/lhernandez-wfscorp), [Avi Boru](https://github.com/aviboru), [Vardhan Annapureddy](https://github.com/harshaaws) | Internal Developer Portal, service catalog product, API's, Software Templates, tech docs and more. |
| [leboncoin](https://www.leboncoin.fr/) | [Andy Ladjadj](https://github.com/aladjadj) | Centralize our multiple UI in a single portal. Simplify onbording, new features and harmonize how people search information. Core features (catalog,api,docs,scafolder) are good to start the adoption. status: internal beta. |
| [Contentful](https://www.contentful.com) | [James Bourne](https://github.com/jamesmbourne) | Centralized documentation of service ownership, APIs, and documentation, and new service creation with a custom scaffolder - [full case study with Roadie](https://roadie.io/case-studies/maintaining-velocity-through-hypergrowth-contentful/). |
| [Back Market](https://www.backmarket.com) | [Sami Farhat](https://github.com/skfarhat) | Internal Developer Portal featuring catalog, tech-radar, ownership management, component creation (scaffolder) and centralized infrastructure management -- probably more to come. |
| [Avalia Systems](https://avalia.io) | [Olivier Liechti](https://github.com/wasadigi), [Fabio Velloso](https://github.com/fabiovelloso) | Innersource, software analytics, knowledge base for 360 software assessments, collaborative applications, hub for tracking and sharing IP assets. |
| [Albert Heijn](https://ah.technology) | [Joost Hofman](https://github.com/joosthofman), [Reindrich Geerman](https://github.com/reinst) | Single point of entry for all our engineers (Developer portal), Tech radar, catalog, templates (paved roads) and tech documentation. |
| [Wise, formerly TransferWise](https://wise.com) | [Andrew Beveridge](https://github.com/beveradb) | It's early days for us, we're trying to start small with catalog, tech docs and unified search. Future ambitious vision includes scaffolder for one-click component addition, building out integrations with CI/CD tooling, kubernetes clusters, monitoring/alerting tooling etc. and aiming for a frictionless "golden path" for engineers! 🚀 |
|[Happy Money](http://happymoney.com/)|[Akshit Lomash](mailto:alomash@happymoney.com)|We are moving from a monolith to microservices-based architecture. We are developing a developer portal based on Backstage to create a service catalog for our new services. All the services created are onboarded Backstage and engineering teams are using a cookie-cutter-based template from backstage to initiate a new service.
|[Lightspeed](http://lightspeedhq.com/)|[Marcus Crane](mailto:marcus.crane@lightspeedhq.com)|We use it within our X-Series division (https://vendhq.com) to catalog ~100+ systems and ~350 components!
|[Siemens](https://www.siemens.com/global/en.html)|[Nizar Chaouch](mailto:nizar.chaouch@siemens.com)|We are using Backstage as our Developer portal
|[The Warehouse Group](https://www.thewarehouse.co.nz)|[Matt Law](mailto:matt.law@thewarehouse.co.nz)|Backstage enables us to bootstrap our middleware environment of new services for our Dev teams in a matter of seconds. CI, CD, testing, logging, deployments are all taken care of to get them up and running in less than 60 seconds.
| [Back Market](https://www.backmarket.com) | [Sami Farhat](https://github.com/skfarhat) | Internal Developer Portal featuring catalog, tech-radar, ownership management, component creation (scaffolder) and centralized infrastructure management -- probably more to come. |
| [Avalia Systems](https://avalia.io) | [Olivier Liechti](https://github.com/wasadigi), [Fabio Velloso](https://github.com/fabiovelloso) | Innersource, software analytics, knowledge base for 360 software assessments, collaborative applications, hub for tracking and sharing IP assets. |
| [Albert Heijn](https://ah.technology) | [Joost Hofman](https://github.com/joosthofman), [Reindrich Geerman](https://github.com/reinst) | Single point of entry for all our engineers (Developer portal), Tech radar, catalog, templates (paved roads) and tech documentation. |
| [Wise, formerly TransferWise](https://wise.com) | [Andrew Beveridge](https://github.com/beveradb) | It's early days for us, we're trying to start small with catalog, tech docs and unified search. Future ambitious vision includes scaffolder for one-click component addition, building out integrations with CI/CD tooling, kubernetes clusters, monitoring/alerting tooling etc. and aiming for a frictionless "golden path" for engineers! 🚀 |
| [Happy Money](http://happymoney.com/) | [Akshit Lomash](mailto:alomash@happymoney.com) | We are moving from a monolith to microservices-based architecture. We are developing a developer portal based on Backstage to create a service catalog for our new services. All the services created are onboarded Backstage and engineering teams are using a cookie-cutter-based template from backstage to initiate a new service. |
| [Lightspeed](http://lightspeedhq.com/) | [Marcus Crane](mailto:marcus.crane@lightspeedhq.com) | We use it within our X-Series division (https://vendhq.com) to catalog ~100+ systems and ~350 components! |
| [Siemens](https://www.siemens.com/global/en.html) | [Nizar Chaouch](mailto:nizar.chaouch@siemens.com) | We are using Backstage as our Developer portal |
| [The Warehouse Group](https://www.thewarehouse.co.nz) | [Matt Law](mailto:matt.law@thewarehouse.co.nz) | Backstage enables us to bootstrap our middleware environment of new services for our Dev teams in a matter of seconds. CI, CD, testing, logging, deployments are all taken care of to get them up and running in less than 60 seconds. |
| [Tink](https://tink.com/) | [Sebastian Olsson](https://github.com/Sebelino), [Błażej Szum](https://github.com/blazejszumtink), [Anders Eurenius Runvald](https://github.com/anders-er-at-tink) | Internal developer portal which provides templates for creating new Java or Go microservices seamlessly. Also includes a tech radar and a visualization of our CD pipeline. |
| [Brandwatch](https://brandwatch.com)| [Stefan Buck](https://github.com/stefanbuck) | Our primary focus is on the service catalog. Backstage is replacing our homemade service catalog. The switch was quite simple due to the catalog processor API.
| [Laybuy](https://www.laybuy.com)| [Chris Simmons](https://github.com/contrarianchris) | Backstage is the heart of Laybuys new centralised Development Platform, bringing disparate development tools and experiences into a single easy-to-use portal. It simplifies software and API discovery, project scaffolding, and technical documentation, enabling us to embrace golden path development and automate software standards.
| [Sendinblue](https://engineering.sendinblue.com/)| [Tanguy Antoine](mailto:antoine.tanguy@sendinblue.com) | Helps us drive the change at scale. Puts light on services, resources, and dependencies. One tool that rules them all through plugins we created for that purpose. We are aiming to put Backstage at the center of every developer's work (Actionable items, Debugging, Monitoring, Provisioning, etc...) to improve their happiness |
| [Brandwatch](https://brandwatch.com) | [Stefan Buck](https://github.com/stefanbuck) | Our primary focus is on the service catalog. Backstage is replacing our homemade service catalog. The switch was quite simple due to the catalog processor API. |
| [Laybuy](https://www.laybuy.com) | [Chris Simmons](https://github.com/contrarianchris) | Backstage is the heart of Laybuys new centralised Development Platform, bringing disparate development tools and experiences into a single easy-to-use portal. It simplifies software and API discovery, project scaffolding, and technical documentation, enabling us to embrace golden path development and automate software standards. |
| [Sendinblue](https://engineering.sendinblue.com/) | [Tanguy Antoine](mailto:antoine.tanguy@sendinblue.com) | Helps us drive the change at scale. Puts light on services, resources, and dependencies. One tool that rules them all through plugins we created for that purpose. We are aiming to put Backstage at the center of every developer's work (Actionable items, Debugging, Monitoring, Provisioning, etc...) to improve their happiness |
| [SafetyCulture](https://safetyculture.com/) | [@R-cen](https://github.com/R-cen), [@lachlancooper](https://github.com/lachlancooper), [@hkf57](https://github.com/hkf57) | Internal developer portal to provide a centralized place for engineers to see an overview of their team's services and information related to the service from other systems. Initially focused on the software catalog, techdocs and search. |
| [Sana Life Science](https://sanalifescience.com) | [Joe Hillyard](mailto:joe@sanalifescience.com) | API Catalog, Tools Management & Control Hub |
| [Ndustrial](https://ndustrial.io) | [Jonathan Skubic](mailto:jonathan@ndustrial.io) | Software Project Catalog |
| [TUI Musement](https://www.musement.com/uk/) | [Simone Fumagalli](mailto:simone.fumagalli@musement.com) | We are importing our catalog into it to keep it under control. The next step is start using templates |
| [Kambi AB](https://www.kambi.com) | [Martin Norum](mailto:martin.norum@kambi.com) | We want to kick ass at speed, so we're currently building up a catalog of our existing software, and looking into how Backstage can support us in our journey towards autonomous product teams. Both to improve speed to market and operational awareness. |
| [ANZ](https://www.anz.com.au/personal/) | [Elliot Jackson](mailto:elliot.jackson@anz.com) | Catalog, tech docs and automation |
| [Genie Solutions](https://www.geniesolutionssoftware.com.au) | [Zainab Bagasrawala](mailto:zainabbagasrawala@geniesolutions.com.au) | Developer Portal to track our projects, documentation, observability tools and more |
| [MadeiraMadeira](https://www.madeiramadeira.com.br) | [Paulo Eduardo Peixoto](mailto:paulo.peixoto@madeiramadeira.com.br) | As a support tool for developers, following the principles of "Developer Experience". In order to make the developer's day to day more practical, efficient and, why not, happy. |
| [Sonatype](https://www.sonatype.com) | [Srikar Ananthula](mailto:sananthula@sonatype.com) | Centralize services used internally with many plugins |
| [CVS Health](https://www.cvshealth.com) | [Ari Ben-Elazar](mailto:abenelazar@gmail.com) | Cataloging and documenting our service offerings to offer our internal developers a better operational journey |
| [Yatra.com](https://www.yatra.com) | [Matiur Rahman Maitur](mailto:arifrahman4u@gmail.com) | Easy to find out Project details, ownership, dependent services, Documentation, it is very useful for developer. |
| [Yotpo](https://www.yotpo.com) | [Liran Yogev](mailto:lyogev@yotpo.com) | Services exploration, documentation and project generator |
| [Mainsail Industries](https://www.mainsailindustries.com) | [Brad Sollar](mailto:brad@mainsailindustries.com) | Internal tool management and docs |
| [Prisma](https://prismamp.com) | [Sebastian Gravina](mailto:sgravina@prismamp.com) | Is part of our IDP |
| [Syndetic](https://syndetic.io) | [John Feminella](mailto:robots+swag@syndetic.io) | We're working with multiple F100 clients for digital transformation and Backstage is a key part of our landing and acceleration strategy. |
| [Imagine Learning](https://www.imaginelearning.com/en/us) | [Jared Stehler](mailto:jared.stehler@imaginelearning.com) | Software catalog, product dependency visualization |
| [Alef Education](https://www.alefeducation.com) | [Belal Juma](mailto:belal.juma@alefeducation.com) | We use backstage as a Service Catalog and rely on the TechDocs feature |
| [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
@@ -3,7 +3,7 @@ title: Contributor Community Sessions
date: May 25, 2022
category: Meetup
description: Join the maintainers and contributors for the Contributor Community Sessions
youtubeUrl: https://youtu.be/evf_LV0KzIk
youtubeUrl: https://youtu.be/neNipVE5ffY
youtubeImgUrl: https://backstage.io/img/b-sessions.png
rsvpUrl: https://calendar.google.com/calendar/embed?src=c_qup9gbhn9sqpuao6trttd8mk5s@group.calendar.google.com
eventUrl: https://github.com/backstage/community/issues/46
+1 -1
View File
@@ -3,7 +3,7 @@ title: Cloudify
author: Cloudify
authorUrl: https://cloudify.co/
category: Orchestration
description: Cloudify provides a remote execution and environment management backend that handles the provisioning and continuous update of Kubernetes, Terraform, Ansible, CloudFormation, Azure ARM, VRO based environments through a single API endpoint.
description: Cloudify provides a remote execution and environment management backend for Kubernetes, Terraform, Ansible, etc.
documentation: https://github.com/cloudify-cosmo/backstage-cloudify-plugin#readme
iconUrl: https://avatars.githubusercontent.com/u/6260555?s=200&v=4
npmPackageName: 'plugin-cloudify'
+3 -3
View File
@@ -2477,9 +2477,9 @@ etag@~1.8.1:
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
eventsource@^1.0.7:
version "1.0.7"
resolved "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==
version "1.1.1"
resolved "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz#4544a35a57d7120fba4fa4c86cb4023b2c09df2f"
integrity sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==
dependencies:
original "^1.0.0"
+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
@@ -93,7 +93,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);
}
}
+2 -2
View File
@@ -41,7 +41,7 @@
"@hot-loader/react-dom-v17": "npm:@hot-loader/react-dom@^17.0.2",
"@manypkg/get-packages": "^1.1.3",
"@octokit/request": "^5.4.12",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-yaml": "^3.1.0",
@@ -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,
@@ -1,5 +1,6 @@
{
"baseUrl": "http://localhost:3001",
"fixturesFolder": false,
"pluginsFile": false
"pluginsFile": false,
"retries": 3
}
@@ -50,7 +50,7 @@
"@types/node": "^14.14.32",
"@types/react-dom": "*",
"cross-env": "^7.0.0",
"cypress": "^7.3.0",
"cypress": "^9.7.0",
"eslint-plugin-cypress": "^2.10.3",
"start-server-and-test": "^1.10.11"
},
@@ -63,7 +63,7 @@
"test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
"test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
"cy:dev": "cypress open",
"cy:run": "cypress run"
"cy:run": "cypress run --browser chrome"
},
"browserslist": {
"production": [
@@ -142,6 +142,30 @@ describe('AzureUrl', () => {
);
});
it('should work with the old tfs long URL', () => {
const url = AzureUrl.fromRepoUrl(
'http://my-host/tfs/projects/my-project/_git/my-repo',
);
expect(url.getOwner()).toBe('tfs/projects');
expect(url.getProject()).toBe('my-project');
expect(url.getRepo()).toBe('my-repo');
expect(url.getRef()).toBeUndefined();
expect(url.getPath()).toBeUndefined();
});
it('should work with the old tfs long URL form with a path and ref', () => {
const url = AzureUrl.fromRepoUrl(
'http://my-host/tfs/projects/my-project/_git/my-repo?path=%2Ffolder&version=GBtest-branch',
);
expect(url.getOwner()).toBe('tfs/projects');
expect(url.getProject()).toBe('my-project');
expect(url.getRepo()).toBe('my-repo');
expect(url.getRef()).toBe('test-branch');
expect(url.getPath()).toBe('/folder');
});
it('should reject non-branch refs', () => {
expect(() =>
AzureUrl.fromRepoUrl(
@@ -37,6 +37,10 @@ export class AzureUrl {
owner = parts[1];
project = parts[2];
repo = parts[4];
} else if (parts[4] === '_git') {
owner = `${parts[1]}/${parts[2]}`;
project = parts[3];
repo = parts[5];
}
if (!owner || !project || !repo) {
@@ -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),
}),
],
});
+26 -4
View File
@@ -284,10 +284,20 @@ export function createPublishGithubAction(options: {
requiredStatusCheckContexts?: string[] | undefined;
repoVisibility?: 'internal' | 'private' | 'public' | undefined;
collaborators?:
| {
username: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}[]
| (
| {
user: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
| {
team: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
| {
username: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
)[]
| undefined;
token?: string | undefined;
topics?: string[] | undefined;
@@ -396,6 +406,10 @@ export class DatabaseTaskStore implements TaskStore {
// (undocumented)
heartbeatTask(taskId: string): Promise<void>;
// (undocumented)
list(options: { createdBy?: string }): Promise<{
tasks: SerializedTask[];
}>;
// (undocumented)
listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{
events: SerializedTaskEvent[];
}>;
@@ -520,6 +534,10 @@ export interface TaskBroker {
// (undocumented)
get(taskId: string): Promise<SerializedTask>;
// (undocumented)
list?(options?: { createdBy?: string }): Promise<{
tasks: SerializedTask[];
}>;
// (undocumented)
vacuumTasks(options: { timeoutS: number }): Promise<void>;
}
@@ -619,6 +637,10 @@ export interface TaskStore {
// (undocumented)
heartbeatTask(taskId: string): Promise<void>;
// (undocumented)
list?(options: { createdBy?: string }): Promise<{
tasks: SerializedTask[];
}>;
// (undocumented)
listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{
events: SerializedTaskEvent[];
}>;
@@ -399,41 +399,35 @@ describe('publish:github', () => {
collaborators: [
{
access: 'pull',
username: 'robot-1',
user: 'robot-1',
},
{
access: 'push',
username: 'robot-2',
team: 'robot-2',
},
],
},
});
const commonProperties = {
org: 'owner',
owner: 'owner',
repo: 'repo',
};
expect(
mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[1],
).toEqual([
{
...commonProperties,
team_slug: 'robot-1',
permission: 'pull',
},
]);
expect(mockOctokit.rest.repos.addCollaborator).toHaveBeenCalledWith({
...commonProperties,
username: 'robot-1',
permission: 'pull',
});
expect(
mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2],
).toEqual([
{
...commonProperties,
team_slug: 'robot-2',
permission: 'push',
},
]);
mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg,
).toHaveBeenCalledWith({
...commonProperties,
org: 'owner',
team_slug: 'robot-2',
permission: 'push',
});
});
it('should ignore failures when adding multiple collaborators', async () => {
@@ -465,11 +459,11 @@ describe('publish:github', () => {
collaborators: [
{
access: 'pull',
username: 'robot-1',
team: 'robot-1',
},
{
access: 'push',
username: 'robot-2',
team: 'robot-2',
},
],
},
@@ -58,10 +58,21 @@ export function createPublishGithubAction(options: {
requireCodeOwnerReviews?: boolean;
requiredStatusCheckContexts?: string[];
repoVisibility?: 'private' | 'internal' | 'public';
collaborators?: Array<{
username: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}>;
collaborators?: Array<
| {
user: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
| {
team: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
| {
/** @deprecated This field is deprecated in favor of team */
username: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
>;
token?: string;
topics?: string[];
}>({
@@ -160,22 +171,39 @@ export function createPublishGithubAction(options: {
},
collaborators: {
title: 'Collaborators',
description: 'Provide additional users with permissions',
description: 'Provide additional users or teams with permissions',
type: 'array',
items: {
type: 'object',
required: ['username', 'access'],
additionalProperties: false,
required: ['access'],
properties: {
access: {
type: 'string',
description: 'The type of access for the user',
enum: ['push', 'pull', 'admin', 'maintain', 'triage'],
},
user: {
type: 'string',
description:
'The name of the user that will be added as a collaborator',
},
username: {
type: 'string',
description: 'The username or group',
description:
'Deprecated. Use the `team` or `user` field instead.',
},
team: {
type: 'string',
description:
'The name of the team that will be added as a collaborator',
},
},
oneOf: [
{ required: ['user'] },
{ required: ['username'] },
{ required: ['team'] },
],
},
},
token: {
@@ -306,22 +334,40 @@ export function createPublishGithubAction(options: {
}
if (collaborators) {
for (const {
access: permission,
username: team_slug,
} of collaborators) {
for (const collaborator of collaborators) {
try {
await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug,
owner,
repo,
permission,
});
if ('user' in collaborator) {
await client.rest.repos.addCollaborator({
owner,
repo,
username: collaborator.user,
permission: collaborator.access,
});
} else if ('username' in collaborator) {
ctx.logger.warn(
'The field `username` is deprecated in favor of `team` and will be removed in the future.',
);
await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: collaborator.username,
owner,
repo,
permission: collaborator.access,
});
} else if ('team' in collaborator) {
await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: collaborator.team,
owner,
repo,
permission: collaborator.access,
});
}
} catch (e) {
assertError(e);
const name = extractCollaboratorName(collaborator);
ctx.logger.warn(
`Skipping ${permission} access for ${team_slug}, ${e.message}`,
`Skipping ${collaborator.access} access for ${name}, ${e.message}`,
);
}
}
@@ -391,3 +437,11 @@ export function createPublishGithubAction(options: {
},
});
}
function extractCollaboratorName(
collaborator: { user: string } | { team: string } | { username: string },
) {
if ('username' in collaborator) return collaborator.username;
if ('user' in collaborator) return collaborator.user;
return collaborator.team;
}
@@ -64,6 +64,14 @@ export type DatabaseTaskStoreOptions = {
database: Knex;
};
const parseSqlDateToIsoString = <T>(input: T): T | string => {
if (typeof input === 'string') {
return DateTime.fromSQL(input, { zone: 'UTC' }).toISO();
}
return input;
};
/**
* DatabaseTaskStore
*
@@ -85,6 +93,31 @@ export class DatabaseTaskStore implements TaskStore {
this.db = options.database;
}
async list(options: {
createdBy?: string;
}): Promise<{ tasks: SerializedTask[] }> {
const queryBuilder = this.db<RawDbTaskRow>('tasks');
if (options.createdBy) {
queryBuilder.where({
created_by: options.createdBy,
});
}
const results = await queryBuilder.orderBy('created_at', 'desc').select();
const tasks = results.map(result => ({
id: result.id,
spec: JSON.parse(result.spec),
status: result.status,
createdBy: result.created_by ?? undefined,
lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at),
createdAt: parseSqlDateToIsoString(result.created_at),
}));
return { tasks };
}
async getTask(taskId: string): Promise<SerializedTask> {
const [result] = await this.db<RawDbTaskRow>('tasks')
.where({ id: taskId })
@@ -99,8 +132,8 @@ export class DatabaseTaskStore implements TaskStore {
id: result.id,
spec,
status: result.status,
lastHeartbeatAt: result.last_heartbeat_at,
createdAt: result.created_at,
lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at),
createdAt: parseSqlDateToIsoString(result.created_at),
createdBy: result.created_by ?? undefined,
secrets,
};
@@ -292,10 +325,7 @@ export class DatabaseTaskStore implements TaskStore {
taskId,
body,
type: event.event_type,
createdAt:
typeof event.created_at === 'string'
? DateTime.fromSQL(event.created_at, { zone: 'UTC' }).toISO()
: event.created_at,
createdAt: parseSqlDateToIsoString(event.created_at),
};
} catch (error) {
throw new Error(
@@ -202,4 +202,31 @@ describe('StorageTaskBroker', () => {
expect(task.done).toBe(true);
});
it('should list all tasks', async () => {
const broker = new StorageTaskBroker(storage, logger);
const { taskId } = await broker.dispatch({ spec: {} as TaskSpec });
const promise = broker.list();
await expect(promise).resolves.toEqual({
tasks: expect.arrayContaining([
expect.objectContaining({
id: taskId,
}),
]),
});
});
it('should list only tasks createdBy a specific user', async () => {
const broker = new StorageTaskBroker(storage, logger);
const { taskId } = await broker.dispatch({
spec: {} as TaskSpec,
createdBy: 'user:default/foo',
});
const task = await storage.getTask(taskId);
const promise = broker.list({ createdBy: 'user:default/foo' });
await expect(promise).resolves.toEqual({ tasks: [task] });
});
});
@@ -150,6 +150,18 @@ export class StorageTaskBroker implements TaskBroker {
private readonly storage: TaskStore,
private readonly logger: Logger,
) {}
async list(options?: {
createdBy?: string;
}): Promise<{ tasks: SerializedTask[] }> {
if (!this.storage.list) {
throw new Error(
'TaskStore does not implement the list method. Please implement the list method to be able to list tasks',
);
}
return await this.storage.list({ createdBy: options?.createdBy });
}
private deferredDispatch = defer();
/**
@@ -133,6 +133,7 @@ export interface TaskBroker {
after: number | undefined;
}): Observable<{ events: SerializedTaskEvent[] }>;
get(taskId: string): Promise<SerializedTask>;
list?(options?: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>;
}
/**
@@ -193,6 +194,7 @@ export interface TaskStore {
listStaleTasks(options: { timeoutS: number }): Promise<{
tasks: { taskId: string }[];
}>;
list?(options: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>;
emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise<void>;
listEvents({
@@ -131,6 +131,7 @@ describe('createRouter', () => {
jest.spyOn(taskBroker, 'dispatch');
jest.spyOn(taskBroker, 'get');
jest.spyOn(taskBroker, 'list');
jest.spyOn(taskBroker, 'event$');
const router = await createRouter({
@@ -216,23 +217,18 @@ describe('createRouter', () => {
const mockToken =
'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob';
console.log(
JSON.stringify(
await request(app)
.post('/v2/tasks')
.set('Authorization', `Bearer ${mockToken}`)
.send({
templateRef: stringifyEntityRef({
kind: 'template',
name: 'create-react-app-template',
}),
values: {
required: 'required-value',
},
}),
),
);
await request(app)
.post('/v2/tasks')
.set('Authorization', `Bearer ${mockToken}`)
.send({
templateRef: stringifyEntityRef({
kind: 'template',
name: 'create-react-app-template',
}),
values: {
required: 'required-value',
},
});
expect(broker).toHaveBeenCalledWith(
expect.objectContaining({
createdBy: 'user:default/guest',
@@ -294,6 +290,77 @@ describe('createRouter', () => {
});
});
describe('GET /v2/tasks', () => {
it('return all tasks', async () => {
(
taskBroker.list as jest.Mocked<Required<TaskBroker>>['list']
).mockResolvedValue({
tasks: [
{
id: 'a-random-id',
spec: {} as any,
status: 'completed',
createdAt: '',
createdBy: '',
},
],
});
const response = await request(app).get(`/v2/tasks`);
expect(taskBroker.list).toBeCalledWith({
createdBy: undefined,
});
expect(response.status).toEqual(200);
expect(response.body).toStrictEqual({
tasks: [
{
id: 'a-random-id',
spec: {} as any,
status: 'completed',
createdAt: '',
createdBy: '',
},
],
});
});
it('return filtered tasks', async () => {
(
taskBroker.list as jest.Mocked<Required<TaskBroker>>['list']
).mockResolvedValue({
tasks: [
{
id: 'a-random-id',
spec: {} as any,
status: 'completed',
createdAt: '',
createdBy: 'user:default/foo',
},
],
});
const response = await request(app).get(
`/v2/tasks?createdBy=user:default/foo`,
);
expect(taskBroker.list).toBeCalledWith({
createdBy: 'user:default/foo',
});
expect(response.status).toEqual(200);
expect(response.body).toStrictEqual({
tasks: [
{
id: 'a-random-id',
spec: {} as any,
status: 'completed',
createdAt: '',
createdBy: 'user:default/foo',
},
],
});
});
});
describe('GET /v2/tasks/:taskId', () => {
it('does not divulge secrets', async () => {
(taskBroker.get as jest.Mocked<TaskBroker>['get']).mockResolvedValue({
@@ -302,6 +369,7 @@ describe('createRouter', () => {
status: 'completed',
createdAt: '',
secrets: { backstageToken: 'secret' },
createdBy: '',
});
const response = await request(app).get(`/v2/tasks/a-random-id`);
@@ -252,6 +252,28 @@ export async function createRouter(
res.status(201).json({ id: result.taskId });
})
.get('/v2/tasks', async (req, res) => {
const [userEntityRef] = [req.query.createdBy].flat();
if (
typeof userEntityRef !== 'string' &&
typeof userEntityRef !== 'undefined'
) {
throw new InputError('createdBy query parameter must be a string');
}
if (!taskBroker.list) {
throw new Error(
'TaskBroker does not support listing tasks, please implement the list method on the TaskBroker.',
);
}
const tasks = await taskBroker.list({
createdBy: userEntityRef,
});
res.status(200).json(tasks);
})
.get('/v2/tasks/:taskId', async (req, res) => {
const { taskId } = req.params;
const task = await taskBroker.get(taskId);
+14
View File
@@ -16,6 +16,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { FieldProps } from '@rjsf/core';
import { FieldValidation } from '@rjsf/core';
import { IdentityApi } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
import { JSONSchema7 } from 'json-schema';
import { JsonValue } from '@backstage/types';
@@ -240,6 +241,14 @@ export interface ScaffolderApi {
templateRef: string,
): Promise<TemplateParameterSchema>;
listActions(): Promise<ListActionsResponse>;
// (undocumented)
listTasks?({
filterByOwnership,
}: {
filterByOwnership: 'owned' | 'all';
}): Promise<{
tasks: ScaffolderTask[];
}>;
scaffold(
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse>;
@@ -255,6 +264,7 @@ export class ScaffolderClient implements ScaffolderApi {
constructor(options: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
identityApi?: IdentityApi;
scmIntegrationsApi: ScmIntegrationRegistry;
useLongPollingLogs?: boolean;
});
@@ -272,6 +282,10 @@ export class ScaffolderClient implements ScaffolderApi {
): Promise<TemplateParameterSchema>;
// (undocumented)
listActions(): Promise<ListActionsResponse>;
// (undocumented)
listTasks(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
tasks: ScaffolderTask[];
}>;
scaffold(
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse>;
+13 -3
View File
@@ -25,7 +25,11 @@ import {
import React from 'react';
import { scaffolderApiRef, ScaffolderClient } from '../src';
import { ScaffolderPage } from '../src/plugin';
import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api';
import {
discoveryApiRef,
fetchApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { CatalogEntityPage } from '@backstage/plugin-catalog';
createDevApp()
@@ -49,9 +53,15 @@ createDevApp()
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
identityApi: identityApiRef,
},
factory: ({ discoveryApi, fetchApi, scmIntegrationsApi }) =>
new ScaffolderClient({ discoveryApi, fetchApi, scmIntegrationsApi }),
factory: ({ discoveryApi, fetchApi, scmIntegrationsApi, identityApi }) =>
new ScaffolderClient({
discoveryApi,
fetchApi,
scmIntegrationsApi,
identityApi,
}),
})
.addPage({
path: '/create',
+1
View File
@@ -57,6 +57,7 @@
"@material-ui/lab": "4.0.0-alpha.57",
"@react-hookz/web": "^13.0.0",
"@rjsf/core": "^3.2.1",
"@material-table/core": "^3.1.0",
"@rjsf/material-ui": "^3.2.1",
"@types/json-schema": "^7.0.9",
"@uiw/react-codemirror": "^4.7.0",
+86
View File
@@ -33,6 +33,13 @@ describe('api', () => {
const discoveryApi = { getBaseUrl: async () => mockBaseUrl };
const fetchApi = new MockFetchApi();
const identityApi = {
getBackstageIdentity: jest.fn(),
getProfileInfo: jest.fn(),
getCredentials: jest.fn(),
signOut: jest.fn(),
};
const scmIntegrationsApi = ScmIntegrations.fromConfig(
new ConfigReader({
integrations: {
@@ -51,7 +58,11 @@ describe('api', () => {
scmIntegrationsApi,
discoveryApi,
fetchApi,
identityApi,
});
jest.restoreAllMocks();
identityApi.getBackstageIdentity.mockReturnValue({});
});
it('should return default and custom integrations', async () => {
@@ -129,6 +140,7 @@ describe('api', () => {
scmIntegrationsApi,
discoveryApi,
fetchApi,
identityApi,
useLongPollingLogs: true,
});
});
@@ -305,4 +317,78 @@ describe('api', () => {
});
});
});
describe('listTasks', () => {
it('should list all tasks', async () => {
server.use(
rest.get(`${mockBaseUrl}/v2/tasks`, (req, res, ctx) => {
const createdBy = req.url.searchParams.get('createdBy');
if (createdBy) {
return res(
ctx.json([
{
createdBy,
},
]),
);
}
return res(
ctx.json([
{
createdBy: null,
},
{
createdBy: null,
},
]),
);
}),
);
const result = await apiClient.listTasks({ filterByOwnership: 'all' });
expect(result).toHaveLength(2);
});
it('should list task using the current user as owner', async () => {
server.use(
rest.get(`${mockBaseUrl}/v2/tasks`, (req, res, ctx) => {
const createdBy = req.url.searchParams.get('createdBy');
if (createdBy) {
return res(
ctx.json({
tasks: [
{
createdBy,
},
],
}),
);
}
return res(
ctx.json({
tasks: [
{
createdBy: null,
},
{
createdBy: null,
},
],
}),
);
}),
);
identityApi.getBackstageIdentity.mockResolvedValueOnce({
userEntityRef: 'user:default/foo',
});
const result = await apiClient.listTasks({ filterByOwnership: 'owned' });
expect(identityApi.getBackstageIdentity).toBeCalled();
expect(result.tasks).toHaveLength(1);
});
});
});
+28
View File
@@ -19,6 +19,7 @@ import {
createApiRef,
DiscoveryApi,
FetchApi,
IdentityApi,
} from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
@@ -39,6 +40,7 @@ import {
ScaffolderDryRunOptions,
ScaffolderDryRunResponse,
} from './types';
import queryString from 'qs';
/**
* Utility API reference for the {@link ScaffolderApi}.
@@ -58,11 +60,13 @@ export class ScaffolderClient implements ScaffolderApi {
private readonly discoveryApi: DiscoveryApi;
private readonly scmIntegrationsApi: ScmIntegrationRegistry;
private readonly fetchApi: FetchApi;
private readonly identityApi?: IdentityApi;
private readonly useLongPollingLogs: boolean;
constructor(options: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
identityApi?: IdentityApi;
scmIntegrationsApi: ScmIntegrationRegistry;
useLongPollingLogs?: boolean;
}) {
@@ -70,6 +74,30 @@ export class ScaffolderClient implements ScaffolderApi {
this.fetchApi = options.fetchApi ?? { fetch };
this.scmIntegrationsApi = options.scmIntegrationsApi;
this.useLongPollingLogs = options.useLongPollingLogs ?? false;
this.identityApi = options.identityApi;
}
async listTasks(options: {
filterByOwnership: 'owned' | 'all';
}): Promise<{ tasks: ScaffolderTask[] }> {
if (!this.identityApi) {
throw new Error(
'IdentityApi is not available in the ScaffolderClient, please pass through the IdentityApi to the ScaffolderClient constructor in order to use the listTasks method',
);
}
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const { userEntityRef } = await this.identityApi.getBackstageIdentity();
const query = queryString.stringify(
options.filterByOwnership === 'owned' ? { createdBy: userEntityRef } : {},
);
const response = await this.fetchApi.fetch(`${baseUrl}/v2/tasks?${query}`);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return await response.json();
}
async getIntegrationsList(
@@ -28,6 +28,7 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
getTask: jest.fn(),
streamLogs: jest.fn(),
listActions: jest.fn(),
listTasks: jest.fn(),
};
const apis = TestApiRegistry.from([scaffolderApiRef, scaffolderApiMock]);

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