Merge branch 'master' into ryanv/product-insights-intervals
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-jenkins': patch
|
||||
---
|
||||
|
||||
Avoid loading data from Jenkins twice. Don't load data when navigating through the pages as all data from all pages is already loaded.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
'@backstage/plugin-sentry': minor
|
||||
'@backstage/plugin-sentry-backend': minor
|
||||
---
|
||||
|
||||
The plugin uses the `proxy-backend` instead of a custom `sentry-backend`.
|
||||
It requires a proxy configuration:
|
||||
|
||||
`app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
'/sentry/api':
|
||||
target: https://sentry.io/api/
|
||||
allowedMethods: ['GET']
|
||||
headers:
|
||||
Authorization:
|
||||
$env: SENTRY_TOKEN # export SENTRY_TOKEN="Bearer <your-sentry-token>"
|
||||
```
|
||||
|
||||
The `MockApiBackend` is no longer configured by the `NODE_ENV` variable.
|
||||
Instead, the mock backend can be used with an api-override:
|
||||
|
||||
`packages/app/src/apis.ts`:
|
||||
|
||||
```ts
|
||||
import { createApiFactory } from '@backstage/core';
|
||||
import { MockSentryApi, sentryApiRef } from '@backstage/plugin-sentry';
|
||||
|
||||
export const apis = [
|
||||
// ...
|
||||
|
||||
createApiFactory(sentryApiRef, new MockSentryApi()),
|
||||
];
|
||||
```
|
||||
|
||||
If you already use the Sentry backend, you must remove it from the backend:
|
||||
|
||||
Delete `packages/backend/src/plugins/sentry.ts`.
|
||||
|
||||
```diff
|
||||
# packages/backend/package.json
|
||||
|
||||
...
|
||||
"@backstage/plugin-scaffolder-backend": "^0.3.2",
|
||||
- "@backstage/plugin-sentry-backend": "^0.1.3",
|
||||
"@backstage/plugin-techdocs-backend": "^0.3.0",
|
||||
...
|
||||
```
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.html
|
||||
|
||||
const apiRouter = Router();
|
||||
apiRouter.use('/catalog', await catalog(catalogEnv));
|
||||
apiRouter.use('/rollbar', await rollbar(rollbarEnv));
|
||||
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
|
||||
- apiRouter.use('/sentry', await sentry(sentryEnv));
|
||||
apiRouter.use('/auth', await auth(authEnv));
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/graphql', await graphql(graphqlEnv));
|
||||
apiRouter.use(notFoundHandler());
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': minor
|
||||
---
|
||||
|
||||
Add support for multiple types of entity cost breakdown.
|
||||
|
||||
This change is backwards-incompatible with plugin-cost-insights 0.3.x; the `entities` field on Entity returned in product cost queries changed from `Entity[]` to `Record<string, Entity[]`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': minor
|
||||
---
|
||||
|
||||
Remove calendar MoM period option and fix quarter end date logic
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Update experimental backend bundle command to only output archives to `dist/` instead of a full workspace mirror in `dist-workspace/`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Fixes a big in the bundling logic that caused `node_modules` inside local monorepo packages to be transformed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
refreshAllLocations uses a child logger of the HigherOrderOperation with a meta `component` : `catalog-all-locations-refresh`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Batch the fetching of relations
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': patch
|
||||
---
|
||||
|
||||
fix react-hooks/exhaustive-deps error
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Add `"files": ["dist"]` to both app and backend packages. This ensures that packaged versions of these packages do not contain unnecessary files.
|
||||
|
||||
To apply this change to an existing app, add the following to `packages/app/package.json` and `packages/backend/package.json`:
|
||||
|
||||
```json
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Move the core url and auth logic to integration for the four major providers
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Add the basics of cross-integration concerns
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
'@backstage/plugin-jenkins': patch
|
||||
---
|
||||
|
||||
Improve loading speed of the CI/CD page.
|
||||
Only request the necessary fields from Jenkins to keep the request size low.
|
||||
In addition everything is loaded in a single request, instead of requesting
|
||||
each job and build individually. As this (and also the previous behavior) can
|
||||
lead to a big amount of data, this limits the amount of jobs to 50.
|
||||
For each job, only the latest build is loaded. Loading the full build history
|
||||
of a job can lead to excessive load on the Jenkins instance.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
Fix React warning of descendant paragraph tag
|
||||
+48
-18
@@ -1,21 +1,34 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import prom from 'prom-client';
|
||||
import promBundle from 'express-prom-bundle';
|
||||
---
|
||||
'@backstage/backend-common': minor
|
||||
---
|
||||
|
||||
Removes the Prometheus integration from `backend-common`.
|
||||
|
||||
Rational behind this change is to keep the metrics integration of Backstage
|
||||
generic. Instead of directly relying on Prometheus, Backstage will expose
|
||||
metrics in a generic way. Integrators can then export the metrics in their
|
||||
desired format. For example using Prometheus.
|
||||
|
||||
To keep the existing behavior, you need to integrate Prometheus in your
|
||||
backend:
|
||||
|
||||
First, add a dependency on `express-prom-bundle` and `prom-client` to your backend.
|
||||
|
||||
```diff
|
||||
// packages/backend/package.json
|
||||
"dependencies": {
|
||||
+ "express-prom-bundle": "^6.1.0",
|
||||
+ "prom-client": "^12.0.0",
|
||||
```
|
||||
|
||||
Then, add a handler for metrics and a simple instrumentation for the endpoints.
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/metrics.ts
|
||||
import { useHotCleanup } from '@backstage/backend-common';
|
||||
import { RequestHandler } from 'express';
|
||||
import promBundle from 'express-prom-bundle';
|
||||
import prom from 'prom-client';
|
||||
import * as url from 'url';
|
||||
|
||||
const rootRegEx = new RegExp('^/([^/]*)/.*');
|
||||
@@ -38,7 +51,7 @@ export function normalizePath(req: any): string {
|
||||
*/
|
||||
export function metricsHandler(): RequestHandler {
|
||||
// We can only initialize the metrics once and have to clean them up between hot reloads
|
||||
prom.register.clear();
|
||||
useHotCleanup(module, () => prom.register.clear());
|
||||
|
||||
return promBundle({
|
||||
includeMethod: true,
|
||||
@@ -51,3 +64,20 @@ export function metricsHandler(): RequestHandler {
|
||||
promClient: { collectDefaultMetrics: {} },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Last, extend your router configuration with the `metricsHandler`:
|
||||
|
||||
```diff
|
||||
+import { metricsHandler } from './metrics';
|
||||
|
||||
...
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.loadConfig(config)
|
||||
.addRouter('', await healthcheck(healthcheckEnv))
|
||||
+ .addRouter('', metricsHandler())
|
||||
.addRouter('/api', apiRouter);
|
||||
```
|
||||
|
||||
Your Prometheus metrics will be available at the `/metrics` endpoint.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/core-api': patch
|
||||
---
|
||||
|
||||
Deprecated the `ConcreteRoute`, `MutableRouteRef`, `AbsoluteRouteRef` types and added a new `RouteRef` type as replacement.
|
||||
|
||||
Deprecated and disabled the `createSubRoute` method of `AbsoluteRouteRef`.
|
||||
|
||||
Add an as of yet unused `params` option to `createRouteRef`.
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': minor
|
||||
---
|
||||
|
||||
Stop exposing a custom router from the `api-docs` plugin. Instead, use the
|
||||
widgets exported by the plugin to compose your custom entity pages.
|
||||
|
||||
Instead of displaying the API definitions directly in the API tab of the
|
||||
component, it now contains tables linking to the API entities. This also adds
|
||||
new widgets to display relationships (bot provides & consumes relationships)
|
||||
between components and APIs.
|
||||
|
||||
See the changelog of `create-app` for a migration guide.
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Adjust template to the latest changes in the `api-docs` plugin.
|
||||
|
||||
## Template Changes
|
||||
|
||||
While updating to the latest `api-docs` plugin, the following changes are
|
||||
necessary for the `create-app` template in your
|
||||
`app/src/components/catalog/EntityPage.tsx`. This adds:
|
||||
|
||||
- A custom entity page for API entities
|
||||
- Changes the API tab to include the new `ConsumedApisCard` and
|
||||
`ProvidedApisCard` that link to the API entity.
|
||||
|
||||
```diff
|
||||
import {
|
||||
+ ApiDefinitionCard,
|
||||
- Router as ApiDocsRouter,
|
||||
+ ConsumedApisCard,
|
||||
+ ProvidedApisCard,
|
||||
+ ConsumedApisCard,
|
||||
+ ConsumingComponentsCard,
|
||||
+ ProvidedApisCard,
|
||||
+ ProvidingComponentsCard
|
||||
} from '@backstage/plugin-api-docs';
|
||||
|
||||
...
|
||||
|
||||
+const ComponentApisContent = ({ entity }: { entity: Entity }) => (
|
||||
+ <Grid container spacing={3} alignItems="stretch">
|
||||
+ <Grid item md={6}>
|
||||
+ <ProvidedApisCard entity={entity} />
|
||||
+ </Grid>
|
||||
+ <Grid item md={6}>
|
||||
+ <ConsumedApisCard entity={entity} />
|
||||
+ </Grid>
|
||||
+ </Grid>
|
||||
+);
|
||||
|
||||
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="/"
|
||||
title="Overview"
|
||||
element={<OverviewContent entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/ci-cd/*"
|
||||
title="CI/CD"
|
||||
element={<CICDSwitcher entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/api/*"
|
||||
title="API"
|
||||
- element={<ApiDocsRouter entity={entity} />}
|
||||
+ element={<ComponentApisContent entity={entity} />}
|
||||
/>
|
||||
...
|
||||
|
||||
-export const EntityPage = () => {
|
||||
- const { entity } = useEntity();
|
||||
- switch (entity?.spec?.type) {
|
||||
- case 'service':
|
||||
- return <ServiceEntityPage entity={entity} />;
|
||||
- case 'website':
|
||||
- return <WebsiteEntityPage entity={entity} />;
|
||||
- default:
|
||||
- return <DefaultEntityPage entity={entity} />;
|
||||
- }
|
||||
-};
|
||||
|
||||
+export const ComponentEntityPage = ({ entity }: { entity: Entity }) => {
|
||||
+ switch (entity?.spec?.type) {
|
||||
+ case 'service':
|
||||
+ return <ServiceEntityPage entity={entity} />;
|
||||
+ case 'website':
|
||||
+ return <WebsiteEntityPage entity={entity} />;
|
||||
+ default:
|
||||
+ return <DefaultEntityPage entity={entity} />;
|
||||
+ }
|
||||
+};
|
||||
+
|
||||
+const ApiOverviewContent = ({ entity }: { entity: Entity }) => (
|
||||
+ <Grid container spacing={3}>
|
||||
+ <Grid item md={6}>
|
||||
+ <AboutCard entity={entity} />
|
||||
+ </Grid>
|
||||
+ <Grid container item md={12}>
|
||||
+ <Grid item md={6}>
|
||||
+ <ProvidingComponentsCard entity={entity} />
|
||||
+ </Grid>
|
||||
+ <Grid item md={6}>
|
||||
+ <ConsumingComponentsCard entity={entity} />
|
||||
+ </Grid>
|
||||
+ </Grid>
|
||||
+ </Grid>
|
||||
+);
|
||||
+
|
||||
+const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => (
|
||||
+ <Grid container spacing={3}>
|
||||
+ <Grid item xs={12}>
|
||||
+ <ApiDefinitionCard apiEntity={entity} />
|
||||
+ </Grid>
|
||||
+ </Grid>
|
||||
+);
|
||||
+
|
||||
+const ApiEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
+ <EntityPageLayout>
|
||||
+ <EntityPageLayout.Content
|
||||
+ path="/*"
|
||||
+ title="Overview"
|
||||
+ element={<ApiOverviewContent entity={entity} />}
|
||||
+ />
|
||||
+ <EntityPageLayout.Content
|
||||
+ path="/definition/*"
|
||||
+ title="Definition"
|
||||
+ element={<ApiDefinitionContent entity={entity as ApiEntity} />}
|
||||
+ />
|
||||
+ </EntityPageLayout>
|
||||
+);
|
||||
+
|
||||
+export const EntityPage = () => {
|
||||
+ const { entity } = useEntity();
|
||||
+
|
||||
+ switch (entity?.kind?.toLowerCase()) {
|
||||
+ case 'component':
|
||||
+ return <ComponentEntityPage entity={entity} />;
|
||||
+ case 'api':
|
||||
+ return <ApiEntityPage entity={entity} />;
|
||||
+ default:
|
||||
+ return <DefaultEntityPage entity={entity} />;
|
||||
+ }
|
||||
+};
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/catalog-model': minor
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
Remove the deprecated fields `ancestors` and `descendants` from the `Group` entity.
|
||||
|
||||
See https://github.com/backstage/backstage/issues/3049 and the PRs linked from it for details.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': minor
|
||||
---
|
||||
|
||||
Removed modifyCss transformer and moved the css to injectCss transformer
|
||||
Fixed issue where some internal doc links would cause a reload of the page
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/theme': patch
|
||||
---
|
||||
|
||||
Add a little more padding in dense tables
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
Export the `defaultConfigLoader` implementation
|
||||
@@ -67,9 +67,9 @@ Firekube
|
||||
freben
|
||||
Fredrik
|
||||
github
|
||||
Github
|
||||
GitHub
|
||||
gitlab
|
||||
Gitlab
|
||||
GitLab
|
||||
Grafana
|
||||
graphql
|
||||
graphviz
|
||||
@@ -198,6 +198,7 @@ talkdesk
|
||||
Talkdesk
|
||||
tasklist
|
||||
techdocs
|
||||
Telenor
|
||||
templated
|
||||
templater
|
||||
Templater
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
- name: prepare nightly release
|
||||
run: yarn changeset version --snapshot nightly
|
||||
|
||||
# Publishes the nightly release to NPM, by using tag we make sure the release is
|
||||
# Publishes the nightly release to npm, by using tag we make sure the release is
|
||||
# not flagged as the latest release, which means that people will not get this
|
||||
# version of the package unless requested explicitly
|
||||
- name: publish nightly release
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Automatically add new TechDocs Issues and PRs to the GitHub project board
|
||||
# Development of TechDocs in Backstage is managed by this Kanban board - https://github.com/orgs/backstage/projects/1
|
||||
# New issues and PRs with TechDocs in their title or docs-like-code label will be added to the board.
|
||||
# Caveat: New PRs created from forks will not be added since GitHub actions don't share credentials with forks.
|
||||
# Caveat: New PRs created from forks will not be added since GitHub Actions don't share credentials with forks.
|
||||
|
||||
on:
|
||||
issues:
|
||||
|
||||
+16
-15
@@ -1,15 +1,16 @@
|
||||
| Organization | Contact | Description of Use |
|
||||
| -------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
|
||||
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
|
||||
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
|
||||
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
|
||||
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
|
||||
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
|
||||
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
|
||||
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
|
||||
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
|
||||
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
|
||||
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
|
||||
| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling |
|
||||
| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling |
|
||||
| Organization | Contact | Description of Use |
|
||||
| -------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
|
||||
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
|
||||
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
|
||||
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
|
||||
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
|
||||
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
|
||||
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
|
||||
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
|
||||
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
|
||||
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
|
||||
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
|
||||
| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling |
|
||||
| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling |
|
||||
| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks |
|
||||
|
||||
@@ -58,6 +58,13 @@ proxy:
|
||||
Authorization:
|
||||
$env: BUILDKITE_TOKEN
|
||||
|
||||
'/sentry/api':
|
||||
target: https://sentry.io/api/
|
||||
allowedMethods: ['GET']
|
||||
headers:
|
||||
Authorization:
|
||||
$env: SENTRY_TOKEN
|
||||
|
||||
organization:
|
||||
name: My Company
|
||||
|
||||
|
||||
@@ -20,4 +20,5 @@ stringData:
|
||||
AZURE_TOKEN: {{ .Values.auth.azure.api.token }}
|
||||
NEW_RELIC_REST_API_KEY: {{ .Values.auth.newRelicRestApiKey }}
|
||||
TRAVISCI_AUTH_TOKEN: {{ .Values.auth.travisciAuthToken }}
|
||||
PAGERDUTY_TOKEN: {{ .Values.auth.pagerdutyToken }}
|
||||
{{- end }}
|
||||
|
||||
@@ -250,3 +250,4 @@ auth:
|
||||
gitlabToken: g
|
||||
newRelicRestApiKey: r
|
||||
travisciAuthToken: fake-travis-ci-auth-token
|
||||
pagerdutyToken: h
|
||||
|
||||
+56
@@ -117,6 +117,62 @@ through the proxy.
|
||||
Learn more about [the different components](overview/what-is-backstage.md) that
|
||||
make up Backstage.
|
||||
|
||||
### How do I keep my Backstage app up to date?
|
||||
|
||||
In many ways one can view Backstage as a library rather than an application or
|
||||
service. The `@backstage/create-app` tool that is used to create your own
|
||||
Backstage app is similar to
|
||||
[`create-react-app`](https://github.com/facebook/create-react-app) in that it
|
||||
gives you a starting point. The code you get is meant to be evolved, and most of
|
||||
the functionality you get out of the box is brought in via npm dependencies.
|
||||
Keeping your app up to date generally means keeping your dependencies up to
|
||||
date. The Backstage CLI provides a command to help you with that. Simply run
|
||||
`yarn backstage-cli versions:bump` at the root of your repo, and the latest
|
||||
versions of all Backstage packages will be installed.
|
||||
|
||||
While staying up to date with new releases and changes will keep your app up to
|
||||
date, it can often be convenient to use the changes done to the
|
||||
`@backstage/create-app` template as another method to stay up to date. For that
|
||||
purpose, any changes done to the template are documented along with upgrade
|
||||
instructions in the
|
||||
[changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md)
|
||||
of the `@backstage/create-app` package.
|
||||
|
||||
### Why can't I dynamically install plugins without modifications the app?
|
||||
|
||||
This decision is part of the core architecture and development flow of
|
||||
Backstage. Plugins have a lot of freedom in what they provide and how they are
|
||||
integrated into the app, and it would therefore add a lot of complexity to allow
|
||||
plugins to be integrated via configuration the same way as they can be
|
||||
integrated with code.
|
||||
|
||||
By bundling all plugins and their dependencies into one app bundle it is also
|
||||
possible to do significant optimizations to the app load time by allowing
|
||||
plugins to share dependencies between each other when possible. This contributes
|
||||
to Backstage being fast, which is an important part of the user and developer
|
||||
experience.
|
||||
|
||||
### Why are there no published Docker images or helm charts for Backstage?
|
||||
|
||||
As mentioned above, Backstage is not a packaged service that you can use out of
|
||||
the box. In order to get started with Backstage you need to use the
|
||||
`@backstage/create-app` package to create and customize your own Backstage app.
|
||||
|
||||
In order to build a Docker image from your own app, you can use the
|
||||
`yarn build-image` command which is included out of the box in the app template.
|
||||
By default this image will bundle up both the frontend and the backend into a
|
||||
single image that you can deploy using your favorite tooling.
|
||||
|
||||
There are also some examples that can help you deploy Backstage to kubernetes in
|
||||
the
|
||||
[contrib](https://github.com/backstage/backstage/tree/master/contrib/kubernetes)
|
||||
folder.
|
||||
|
||||
It is possible that example images will be provided in the future, which can be
|
||||
used to quickly try out a small subset of the functionality of Backstage, but
|
||||
these would not be able to provide much more functionality on top of what you
|
||||
can see on a demo site.
|
||||
|
||||
### Do I have to write plugins in TypeScript?
|
||||
|
||||
No, you can use JavaScript if you prefer. We want to keep the Backstage core
|
||||
|
||||
@@ -58,7 +58,7 @@ discover existing functionality in the ecosystem.
|
||||
APIs are implemented by components and make their boundaries explicit. They
|
||||
might be defined using an RPC IDL (e.g. in Protobuf, GraphQL or similar), a data
|
||||
schema (e.g. in Avro, TFRecord or similar), or as code interfaces (e.g.
|
||||
framework APIs in Swift, Kotlin, Java, C++, Typescript etc). In any case, APIs
|
||||
framework APIs in Swift, Kotlin, Java, C++, TypeScript etc). In any case, APIs
|
||||
exposed by components need to be in a known machine-readable format so we can
|
||||
build further tooling and analysis on top.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Architecture Decision Record (ADR) log on Avoid React.FC and React.
|
||||
|
||||
## Context
|
||||
|
||||
Facebook has removed `React.FC` from their base template for a Typescript
|
||||
Facebook has removed `React.FC` from their base template for a TypeScript
|
||||
project. The reason for this was that it was found to be an unnecessary feature
|
||||
with next to no benefits in combination with a few downsides.
|
||||
|
||||
|
||||
@@ -407,7 +407,7 @@ The current set of well-known and common values for this field is:
|
||||
|
||||
- `service` - a backend service, typically exposing an API
|
||||
- `website` - a website
|
||||
- `library` - a software library, such as an NPM module or a Java library
|
||||
- `library` - a software library, such as an npm module or a Java library
|
||||
|
||||
### `spec.lifecycle` [required]
|
||||
|
||||
@@ -558,7 +558,7 @@ The current set of well-known and common values for this field is:
|
||||
|
||||
- `service` - a backend service, typically exposing an API
|
||||
- `website` - a website
|
||||
- `library` - a software library, such as an NPM module or a Java library
|
||||
- `library` - a software library, such as an npm module or a Java library
|
||||
|
||||
### `spec.templater` [required]
|
||||
|
||||
@@ -724,9 +724,7 @@ metadata:
|
||||
spec:
|
||||
type: business-unit
|
||||
parent: ops
|
||||
ancestors: [ops, global-synergies, acme-corp]
|
||||
children: [backstage, other]
|
||||
descendants: [backstage, other, team-a, team-b, team-c, team-d]
|
||||
```
|
||||
|
||||
In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata)
|
||||
@@ -762,25 +760,6 @@ namespace as the user. Only `Group` entities may be referenced. Most commonly,
|
||||
this field points to a group in the same namespace, so in those cases it is
|
||||
sufficient to enter only the `metadata.name` field of that group.
|
||||
|
||||
### `spec.ancestors` [required]
|
||||
|
||||
**NOTE**: This field was marked for deprecation on Nov 22nd, 2020. It will be
|
||||
removed entirely from the model on Dec 6th, 2020 in the repository and will not
|
||||
be present in released packages following the next release after that. Please
|
||||
update your code to not consume this field before the removal date.
|
||||
|
||||
The recursive list of parents up the hierarchy, by stepping through parents one
|
||||
by one. The list must be present, but may be empty if `parent` is not present.
|
||||
The first entry in the list is equal to `parent`, and then the following ones
|
||||
are progressively farther up the hierarchy.
|
||||
|
||||
The entries of this array are
|
||||
[entity references](https://backstage.io/docs/features/software-catalog/references),
|
||||
with the default kind `Group` and the default namespace equal to the same
|
||||
namespace as the user. Only `Group` entities may be referenced. Most commonly,
|
||||
these entries point to groups in the same namespace, so in those cases it is
|
||||
sufficient to enter only the `metadata.name` field of those groups.
|
||||
|
||||
### `spec.children` [required]
|
||||
|
||||
The immediate child groups of this group in the hierarchy (whose `parent` field
|
||||
@@ -795,25 +774,6 @@ namespace as the user. Only `Group` entities may be referenced. Most commonly,
|
||||
these entries point to groups in the same namespace, so in those cases it is
|
||||
sufficient to enter only the `metadata.name` field of those groups.
|
||||
|
||||
### `spec.descendants` [required]
|
||||
|
||||
**NOTE**: This field was marked for deprecation on Nov 22nd, 2020. It will be
|
||||
removed entirely from the model on Dec 6th, 2020 in the repository and will not
|
||||
be present in released packages following the next release after that. Please
|
||||
update your code to not consume this field before the removal date.
|
||||
|
||||
The immediate and recursive child groups of this group in the hierarchy
|
||||
(children, and children's children, etc.). The list must be present, but may be
|
||||
empty if there are no child groups. The items are not guaranteed to be ordered
|
||||
in any particular way.
|
||||
|
||||
The entries of this array are
|
||||
[entity references](https://backstage.io/docs/features/software-catalog/references),
|
||||
with the default kind `Group` and the default namespace equal to the same
|
||||
namespace as the user. Only `Group` entities may be referenced. Most commonly,
|
||||
these entries point to groups in the same namespace, so in those cases it is
|
||||
sufficient to enter only the `metadata.name` field of those groups.
|
||||
|
||||
## Kind: User
|
||||
|
||||
Describes the following entity kind:
|
||||
|
||||
@@ -21,7 +21,7 @@ looking at. In response, it receives the static files (HTML, CSS, JSON, etc.) to
|
||||
render on the page in TechDocs/Backstage.
|
||||
|
||||
The static files consist of HTML, CSS and Images generated by MkDocs. We remove
|
||||
all the Javascript before adding them to Backstage for security reasons. And
|
||||
all the JavaScript before adding them to Backstage for security reasons. And
|
||||
there are some additional techdocs metadata JSON files that TechDocs needs to
|
||||
render a site.
|
||||
|
||||
@@ -59,7 +59,7 @@ Similar to how it is done in the Basic setup, the TechDocs Reader requests
|
||||
your configured storage solution for the necessary files and returns them to
|
||||
TechDocs Reader.
|
||||
|
||||
We will provide instructions, scripts and/or templates (e.g. GitHub actions) to
|
||||
We will provide instructions, scripts and/or templates (e.g. GitHub Actions) to
|
||||
build docs in your CI/CD system.
|
||||
[Track progress here.](https://github.com/backstage/backstage/issues/3400) You
|
||||
will be able to use `techdocs-cli` to build docs and publish the generated docs
|
||||
|
||||
@@ -11,7 +11,7 @@ add an existing plugin to it. We are using the
|
||||
[CircleCI](https://github.com/backstage/backstage/blob/master/plugins/circleci/README.md)
|
||||
plugin in this example.
|
||||
|
||||
1. Add the plugin's NPM package to the repo:
|
||||
1. Add the plugin's npm package to the repo:
|
||||
|
||||
```bash
|
||||
yarn add @backstage/plugin-circleci
|
||||
|
||||
@@ -87,7 +87,7 @@ You may encounter the following error message:
|
||||
Couldn't find any versions for "file-saver" that matches "eligrey-FileSaver.js-1.3.8.tar.gz-art-external"
|
||||
```
|
||||
|
||||
This is likely because you have a globally configured NPM proxy, which breaks
|
||||
This is likely because you have a globally configured npm proxy, which breaks
|
||||
the installation of the `material-table` dependency. This is a known issue and
|
||||
being worked on in `material-table`, but for now you can work around it using
|
||||
the following:
|
||||
|
||||
@@ -10,7 +10,7 @@ you're planning to do.
|
||||
|
||||
Creating a standalone instance makes it simpler to customize the application for
|
||||
your needs whilst staying up to date with the project. You will also depend on
|
||||
`@backstage` packages from NPM, making the project much smaller. This is the
|
||||
`@backstage` packages from npm, making the project much smaller. This is the
|
||||
recommended approach if you want to kick the tyres of Backstage or setup your
|
||||
own instance.
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
id: publishing
|
||||
title: Publishing
|
||||
description: Documentation on Publishing NPM packages
|
||||
description: Documentation on Publishing npm packages
|
||||
---
|
||||
|
||||
## NPM
|
||||
## npm
|
||||
|
||||
NPM packages are published through CI/CD in the
|
||||
npm packages are published through CI/CD in the
|
||||
[.github/workflows/master.yml](https://github.com/backstage/backstage/blob/master/.github/workflows/master.yml)
|
||||
workflow. Every commit that is merged to master will be checked for new versions
|
||||
of all public packages, and any new versions will automatically be published to
|
||||
NPM.
|
||||
npm.
|
||||
|
||||
### Creating a new release
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ This is especially true for edge cases!
|
||||
|
||||
## Non-React Classes
|
||||
|
||||
Testing a Javascript object which is _not_ a React component follows a lot of
|
||||
Testing a JavaScript object which is _not_ a React component follows a lot of
|
||||
the same principles as testing objects in other languages.
|
||||
|
||||
### API Testing Principles
|
||||
@@ -243,7 +243,7 @@ Testing an API involves verifying four things:
|
||||
|
||||
1. Invalid inputs are caught before being sent to the server.
|
||||
2. Valid inputs translate into a valid browser request.
|
||||
3. Server response is translated into an expected Javascript object.
|
||||
3. Server response is translated into an expected JavaScript object.
|
||||
4. Server errors are handled gracefully.
|
||||
|
||||
### Mocking API Calls
|
||||
|
||||
@@ -161,7 +161,7 @@ are separated out into their own folder, see further down.
|
||||
|
||||
- [`docgen/`](https://github.com/backstage/backstage/tree/master/packages/docgen) -
|
||||
Uses the
|
||||
[Typescript Compiler API](https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API)
|
||||
[TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API)
|
||||
to read out definitions and generate documentation for it.
|
||||
|
||||
- [`e2e-test/`](https://github.com/backstage/backstage/tree/master/packages/e2e-test) -
|
||||
|
||||
@@ -22,7 +22,7 @@ music and wants to have a theme tune for every service in Backstage.
|
||||
|
||||
Sam built a Spotify plugin for Backstage that allows service owners to define a
|
||||
theme tune for their service. The theme tune plays whenever a user visits the
|
||||
service page in Backstage. The plugin is published to NPM and available for any
|
||||
service page in Backstage. The plugin is published to npm and available for any
|
||||
organization to easily install and add to their Backstage installation.
|
||||
|
||||
# 1. A New Plugin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Announcing Backstage
|
||||
author: Stefan Ålund
|
||||
author: Stefan Ålund, Spotify
|
||||
authorURL: http://twitter.com/stalund
|
||||
authorImageURL: https://pbs.twimg.com/profile_images/121166861/6919c047c0d0edaace78c3009b28e917-user-full-200-130.generated_400x400.jpg
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: What the heck is Backstage anyway?
|
||||
author: Stefan Ålund
|
||||
author: Stefan Ålund, Spotify
|
||||
authorURL: http://twitter.com/stalund
|
||||
authorImageURL: https://pbs.twimg.com/profile_images/121166861/6919c047c0d0edaace78c3009b28e917-user-full-200-130.generated_400x400.jpg
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Introducing Lighthouse for Backstage
|
||||
author: Paul Marbach
|
||||
author: Paul Marbach, Spotify
|
||||
authorURL: http://twitter.com/fastfrwrd
|
||||
authorImageURL: https://pbs.twimg.com/profile_images/1224058798958088192/JPxS8uzR_400x400.jpg
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: How to quickly set up Backstage
|
||||
author: Marcus Eide
|
||||
author: Marcus Eide, Spotify
|
||||
authorURL: https://github.com/marcuseide
|
||||
authorImageURL: https://secure.gravatar.com/avatar/20223f1e03673c7c1e6282fbebaf6942
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Introducing Tech Radar for Backstage
|
||||
author: Bilawal Hameed
|
||||
author: Bilawal Hameed, Spotify
|
||||
authorURL: http://twitter.com/bilawalhameed
|
||||
authorImageURL: https://avatars0.githubusercontent.com/bih
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Weaveworks’ COVID-19 app uses Backstage UI
|
||||
author: Jeff Feng
|
||||
author: Jeff Feng, Spotify
|
||||
authorURL: https://github.com/fengypants
|
||||
authorImageURL: https://avatars2.githubusercontent.com/u/46946747
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Starting Phase 2: The Service Catalog
|
||||
author: Stefan Ålund
|
||||
author: Stefan Ålund, Spotify
|
||||
authorURL: http://twitter.com/stalund
|
||||
authorImageURL: https://pbs.twimg.com/profile_images/121166861/6919c047c0d0edaace78c3009b28e917-user-full-200-130.generated_400x400.jpg
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Backstage Service Catalog released in alpha
|
||||
author: Stefan Ålund
|
||||
author: Stefan Ålund, Spotify
|
||||
authorURL: http://twitter.com/stalund
|
||||
image: https://backstage.io/blog/assets/6/header.png
|
||||
---
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: How to enable authentication in Backstage using Passport
|
||||
author: Lee Mills
|
||||
author: Lee Mills, Spotify
|
||||
authorURL: https://github.com/leemills83
|
||||
authorImageURL: https://avatars1.githubusercontent.com/u/1236238?s=460&v=4
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Announcing Backstage Software Templates
|
||||
author: Stefan Ålund
|
||||
author: Stefan Ålund, Spotify
|
||||
authorURL: https://twitter.com/stalund
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Announcing TechDocs: Spotify’s docs-like-code plugin for Backstage
|
||||
author: Gary Niemen
|
||||
author: Gary Niemen, Spotify
|
||||
authorURL: https://github.com/garyniemen
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Backstage has been accepted into the CNCF Sandbox
|
||||
author: Stefan Ålund
|
||||
author: Stefan Ålund, Spotify
|
||||
authorURL: https://twitter.com/stalund
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: How to design for Backstage (even if you’re not a designer)
|
||||
author: Kat Zhou
|
||||
author: Kat Zhou, Spotify
|
||||
authorURL: http://twitter.com/katherinemzhou
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: The Plugin Marketplace is open
|
||||
author: Stefan Ålund
|
||||
author: Stefan Ålund, Spotify
|
||||
authorURL: https://twitter.com/stalund
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: New Cost Insights plugin: The engineer’s solution to taming cloud costs
|
||||
author: Janisa Anandamohan
|
||||
author: Janisa Anandamohan, Spotify
|
||||
authorURL: https://twitter.com/janisa_a
|
||||
---
|
||||
|
||||
|
||||
@@ -54,6 +54,12 @@ class Footer extends React.Component {
|
||||
<a href={this.props.config.fossWebsite}>
|
||||
Open Source @ {this.props.config.organizationName}
|
||||
</a>
|
||||
|
||||
<a href="https://engineering.atspotify.com/">
|
||||
Spotify Engineering Blog
|
||||
</a>
|
||||
<a href="https://developer.spotify.com/">Spotify for Developers</a>
|
||||
|
||||
<a href={this.props.config.repoUrl}>GitHub</a>
|
||||
<a
|
||||
className="github-button"
|
||||
@@ -78,6 +84,9 @@ class Footer extends React.Component {
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<p style={{ textAlign: 'center' }}>
|
||||
<a href="https://spotify.github.io">Made with ❤️ at Spotify</a>
|
||||
</p>
|
||||
<p className="copyright">{this.props.config.copyright}</p>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Argo CD
|
||||
author: roadie.io
|
||||
authorUrl: https://roadie.io
|
||||
category: CI
|
||||
description: View Argo CD status for your projects in Backstage.
|
||||
documentation: https://roadie.io/backstage/plugins/argo-cd
|
||||
iconUrl: https://roadie.io/images/logos/argo.png
|
||||
npmPackageName: '@roadiehq/backstage-plugin-argo-cd'
|
||||
tags:
|
||||
- cd
|
||||
- ci
|
||||
@@ -15,8 +15,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@spotify/prettier-config": "^9.0.0",
|
||||
"docusaurus": "^2.0.0-alpha.66",
|
||||
"js-yaml": "^3.14.0",
|
||||
"docusaurus": "^2.0.0-alpha.378053ac5",
|
||||
"js-yaml": "^3.14.1",
|
||||
"prettier": "^2.2.1"
|
||||
},
|
||||
"prettier": "@spotify/prettier-config"
|
||||
|
||||
@@ -8,11 +8,8 @@
|
||||
// See https://docusaurus.io/docs/site-config for all the possible
|
||||
// site configuration options.
|
||||
|
||||
// List of projects/orgs using your project for the users page.
|
||||
const users = [];
|
||||
|
||||
const siteConfig = {
|
||||
title: 'Backstage', // Title for your website.
|
||||
title: 'Backstage Service Catalog and Developer Platform', // Title for your website.
|
||||
tagline: 'An open platform for building developer portals',
|
||||
url: 'https://backstage.io', // Your website URL
|
||||
cname: 'backstage.io',
|
||||
@@ -72,11 +69,6 @@ const siteConfig = {
|
||||
navGroupSubcategoryTitleColor: '#9e9e9e',
|
||||
},
|
||||
|
||||
/* Colors for syntax highlighting */
|
||||
highlight: {
|
||||
theme: 'dark',
|
||||
},
|
||||
|
||||
// This copyright info is used in /core/Footer.js and blog RSS/Atom feeds.
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Backstage Project Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage`,
|
||||
|
||||
|
||||
+572
-504
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -69,7 +69,7 @@ nav:
|
||||
- Testing:
|
||||
- Overview: 'plugins/testing.md'
|
||||
- Publishing:
|
||||
- Open source and NPM: 'plugins/publishing.md'
|
||||
- Open source and npm: 'plugins/publishing.md'
|
||||
- Private/internal (non-open source): 'plugins/publish-private.md'
|
||||
- Configuration:
|
||||
- Overview: 'conf/index.md'
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@backstage/plugin-github-actions": "^0.2.3",
|
||||
"@backstage/plugin-gitops-profiles": "^0.2.1",
|
||||
"@backstage/plugin-graphiql": "^0.2.1",
|
||||
"@backstage/plugin-org": "^0.3.0",
|
||||
"@backstage/plugin-jenkins": "^0.3.2",
|
||||
"@backstage/plugin-kubernetes": "^0.3.1",
|
||||
"@backstage/plugin-lighthouse": "^0.2.4",
|
||||
@@ -89,5 +90,8 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
||||
@@ -13,11 +13,19 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ApiEntity, Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
ApiEntity,
|
||||
Entity,
|
||||
GroupEntity,
|
||||
UserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
import { EmptyState } from '@backstage/core';
|
||||
import {
|
||||
ApiDefinitionCard,
|
||||
Router as ApiDocsRouter,
|
||||
ConsumedApisCard,
|
||||
ConsumingComponentsCard,
|
||||
ProvidedApisCard,
|
||||
ProvidingComponentsCard,
|
||||
} from '@backstage/plugin-api-docs';
|
||||
import {
|
||||
AboutCard,
|
||||
@@ -48,6 +56,12 @@ import {
|
||||
isPluginApplicableToEntity as isLighthouseAvailable,
|
||||
LastLighthouseAuditCard,
|
||||
} from '@backstage/plugin-lighthouse';
|
||||
import {
|
||||
OwnershipCard,
|
||||
MembersListCard,
|
||||
GroupProfileCard,
|
||||
UserProfileCard,
|
||||
} from '@backstage/plugin-org';
|
||||
import { Router as SentryRouter } from '@backstage/plugin-sentry';
|
||||
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
|
||||
import { Button, Grid } from '@material-ui/core';
|
||||
@@ -176,6 +190,17 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const ComponentApisContent = ({ entity }: { entity: Entity }) => (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
<Grid item md={6}>
|
||||
<ProvidedApisCard entity={entity} />
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<ConsumedApisCard entity={entity} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
@@ -196,7 +221,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout.Content
|
||||
path="/api/*"
|
||||
title="API"
|
||||
element={<ApiDocsRouter entity={entity} />}
|
||||
element={<ComponentApisContent entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/docs/*"
|
||||
@@ -297,6 +322,14 @@ const ApiOverviewContent = ({ entity }: { entity: Entity }) => (
|
||||
<Grid item md={6}>
|
||||
<AboutCard entity={entity} />
|
||||
</Grid>
|
||||
<Grid container item md={12}>
|
||||
<Grid item md={6}>
|
||||
<ProvidingComponentsCard entity={entity} />
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<ConsumingComponentsCard entity={entity} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -323,6 +356,51 @@ const ApiEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
const UserOverviewContent = ({ entity }: { entity: UserEntity }) => (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<UserProfileCard entity={entity} variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<OwnershipCard entity={entity} variant="gridItem" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const UserEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="/*"
|
||||
title="Overview"
|
||||
element={<UserOverviewContent entity={entity as UserEntity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<GroupProfileCard entity={entity} variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<OwnershipCard entity={entity} variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<MembersListCard entity={entity} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const GroupEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="/*"
|
||||
title="Overview"
|
||||
element={<GroupOverviewContent entity={entity as GroupEntity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
export const EntityPage = () => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
@@ -331,6 +409,10 @@ export const EntityPage = () => {
|
||||
return <ComponentEntityPage entity={entity} />;
|
||||
case 'api':
|
||||
return <ApiEntityPage entity={entity} />;
|
||||
case 'group':
|
||||
return <GroupEntityPage entity={entity} />;
|
||||
case 'user':
|
||||
return <UserEntityPage entity={entity} />;
|
||||
default:
|
||||
return <DefaultEntityPage entity={entity} />;
|
||||
}
|
||||
|
||||
@@ -42,3 +42,4 @@ export { plugin as UserSettings } from '@backstage/plugin-user-settings';
|
||||
export { plugin as PagerDuty } from '@backstage/plugin-pagerduty';
|
||||
export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite';
|
||||
export { plugin as Search } from '@backstage/plugin-search';
|
||||
export { plugin as Org } from '@backstage/plugin-org';
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"cors": "^2.8.5",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"express": "^4.17.1",
|
||||
"express-prom-bundle": "^6.1.0",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.1",
|
||||
"git-url-parse": "^11.4.0",
|
||||
@@ -51,7 +50,6 @@
|
||||
"logform": "^2.1.1",
|
||||
"minimist": "^1.2.5",
|
||||
"morgan": "^1.10.0",
|
||||
"prom-client": "^12.0.0",
|
||||
"selfsigned": "^1.10.7",
|
||||
"stoppable": "^1.1.0",
|
||||
"tar": "^6.0.5",
|
||||
|
||||
@@ -20,7 +20,7 @@ import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { getVoidLogger } from '../logging';
|
||||
import { AzureUrlReader, getDownloadUrl } from './AzureUrlReader';
|
||||
import { AzureUrlReader } from './AzureUrlReader';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
import { ReadTreeResponseFactory } from './tree';
|
||||
|
||||
@@ -111,13 +111,13 @@ describe('AzureUrlReader', () => {
|
||||
url: 'https://api.com/a/b/blob/master/path/to/c.yaml',
|
||||
config: createConfig(),
|
||||
error:
|
||||
'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path',
|
||||
'Incorrect URL: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path',
|
||||
},
|
||||
{
|
||||
url: 'com/a/b/blob/master/path/to/c.yaml',
|
||||
config: createConfig(),
|
||||
error:
|
||||
'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
|
||||
'Incorrect URL: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
|
||||
},
|
||||
{
|
||||
url: '',
|
||||
@@ -178,21 +178,4 @@ describe('AzureUrlReader', () => {
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDownloadUrl', () => {
|
||||
it('do not add scopePath if no path is specified', async () => {
|
||||
const result = getDownloadUrl(
|
||||
'https://dev.azure.com/organization/project/_git/repository',
|
||||
);
|
||||
|
||||
expect(result.searchParams.get('scopePath')).toBeNull();
|
||||
});
|
||||
|
||||
it('add scopePath if a path is specified', async () => {
|
||||
const result = getDownloadUrl(
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fdocs',
|
||||
);
|
||||
expect(result.searchParams.get('scopePath')).toEqual('docs');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
import {
|
||||
AzureIntegrationConfig,
|
||||
readAzureIntegrationConfigs,
|
||||
getAzureFileFetchUrl,
|
||||
getAzureDownloadUrl,
|
||||
getAzureRequestOptions,
|
||||
} from '@backstage/integration';
|
||||
import fetch from 'cross-fetch';
|
||||
import { Readable } from 'stream';
|
||||
import parseGitUri from 'git-url-parse';
|
||||
import { NotFoundError } from '../errors';
|
||||
import {
|
||||
ReaderFactory,
|
||||
@@ -30,28 +32,6 @@ import {
|
||||
} from './types';
|
||||
import { ReadTreeResponseFactory } from './tree';
|
||||
|
||||
export function getDownloadUrl(url: string): URL {
|
||||
const {
|
||||
name: repoName,
|
||||
owner: project,
|
||||
organization,
|
||||
protocol,
|
||||
resource,
|
||||
filepath,
|
||||
} = parseGitUri(url);
|
||||
|
||||
// scopePath will limit the downloaded content
|
||||
// /docs will only download the docs folder and everything below it
|
||||
// /docs/index.md will only download index.md but put it in the root of the archive
|
||||
const scopePath = filepath
|
||||
? `&scopePath=${encodeURIComponent(filepath)}`
|
||||
: '';
|
||||
|
||||
return new URL(
|
||||
`${protocol}://${resource}/${organization}/${project}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
export class AzureUrlReader implements UrlReader {
|
||||
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
|
||||
const configs = readAzureIntegrationConfigs(
|
||||
@@ -76,11 +56,11 @@ export class AzureUrlReader implements UrlReader {
|
||||
}
|
||||
|
||||
async read(url: string): Promise<Buffer> {
|
||||
const builtUrl = this.buildRawUrl(url);
|
||||
const builtUrl = getAzureFileFetchUrl(url);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(builtUrl.toString(), this.getRequestOptions());
|
||||
response = await fetch(builtUrl, getAzureRequestOptions(this.options));
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to read ${url}, ${e}`);
|
||||
}
|
||||
@@ -102,8 +82,8 @@ export class AzureUrlReader implements UrlReader {
|
||||
options?: ReadTreeOptions,
|
||||
): Promise<ReadTreeResponse> {
|
||||
const response = await fetch(
|
||||
getDownloadUrl(url).toString(),
|
||||
this.getRequestOptions({ Accept: 'application/zip' }),
|
||||
getAzureDownloadUrl(url),
|
||||
getAzureRequestOptions(this.options, { Accept: 'application/zip' }),
|
||||
);
|
||||
if (!response.ok) {
|
||||
const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`;
|
||||
@@ -119,80 +99,6 @@ export class AzureUrlReader implements UrlReader {
|
||||
});
|
||||
}
|
||||
|
||||
// Converts
|
||||
// from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents
|
||||
// to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch}
|
||||
private buildRawUrl(target: string): URL {
|
||||
try {
|
||||
const url = new URL(target);
|
||||
|
||||
const [
|
||||
empty,
|
||||
userOrOrg,
|
||||
project,
|
||||
srcKeyword,
|
||||
repoName,
|
||||
] = url.pathname.split('/');
|
||||
|
||||
const path = url.searchParams.get('path') || '';
|
||||
const ref = url.searchParams.get('version')?.substr(2);
|
||||
|
||||
if (
|
||||
url.hostname !== 'dev.azure.com' ||
|
||||
empty !== '' ||
|
||||
userOrOrg === '' ||
|
||||
project === '' ||
|
||||
srcKeyword !== '_git' ||
|
||||
repoName === '' ||
|
||||
path === '' ||
|
||||
ref === ''
|
||||
) {
|
||||
throw new Error('Wrong Azure Devops URL or Invalid file path');
|
||||
}
|
||||
|
||||
// transform to api
|
||||
url.pathname = [
|
||||
empty,
|
||||
userOrOrg,
|
||||
project,
|
||||
'_apis',
|
||||
'git',
|
||||
'repositories',
|
||||
repoName,
|
||||
'items',
|
||||
].join('/');
|
||||
|
||||
const queryParams = [`path=${path}`];
|
||||
|
||||
if (ref) {
|
||||
queryParams.push(`version=${ref}`);
|
||||
}
|
||||
|
||||
url.search = queryParams.join('&');
|
||||
|
||||
url.protocol = 'https';
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect url: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
private getRequestOptions(additionalHeaders?: {
|
||||
[key: string]: string;
|
||||
}): RequestInit {
|
||||
const headers: HeadersInit = additionalHeaders ?? {};
|
||||
|
||||
if (this.options.token) {
|
||||
headers.Authorization = `Basic ${Buffer.from(
|
||||
`:${this.options.token}`,
|
||||
'utf8',
|
||||
).toString('base64')}`;
|
||||
}
|
||||
|
||||
return { headers };
|
||||
}
|
||||
|
||||
toString() {
|
||||
const { host, token } = this.options;
|
||||
return `azure{host=${host},authed=${Boolean(token)}}`;
|
||||
|
||||
@@ -14,94 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BitbucketIntegrationConfig } from '@backstage/integration';
|
||||
import {
|
||||
BitbucketUrlReader,
|
||||
getApiRequestOptions,
|
||||
getApiUrl,
|
||||
} from './BitbucketUrlReader';
|
||||
import { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
|
||||
describe('BitbucketUrlReader', () => {
|
||||
describe('getApiRequestOptions', () => {
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getApiRequestOptions(withToken).headers as any).Authorization,
|
||||
).toEqual('Bearer A');
|
||||
expect(
|
||||
(getApiRequestOptions(withoutToken).headers as any).Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('insert basic auth when needed', () => {
|
||||
const withUsernameAndPassword: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
username: 'some-user',
|
||||
appPassword: 'my-secret',
|
||||
};
|
||||
const withoutUsernameAndPassword: BitbucketIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getApiRequestOptions(withUsernameAndPassword).headers as any)
|
||||
.Authorization,
|
||||
).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA==');
|
||||
expect(
|
||||
(getApiRequestOptions(withoutUsernameAndPassword).headers as any)
|
||||
.Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApiUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: BitbucketIntegrationConfig = { host: '', apiBaseUrl: '' };
|
||||
expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
|
||||
});
|
||||
it('happy path for Bitbucket Cloud', () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
};
|
||||
expect(
|
||||
getApiUrl(
|
||||
'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml',
|
||||
),
|
||||
);
|
||||
});
|
||||
it('happy path for Bitbucket Server', () => {
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
expect(
|
||||
getApiUrl(
|
||||
'https://bitbucket.mycompany.net/projects/a/repos/b/browse/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://bitbucket.mycompany.net/rest/api/1.0/projects/a/repos/b/raw/path/to/c.yaml',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('implementation', () => {
|
||||
it('rejects unknown targets', async () => {
|
||||
const processor = new BitbucketUrlReader({
|
||||
|
||||
@@ -16,69 +16,14 @@
|
||||
|
||||
import {
|
||||
BitbucketIntegrationConfig,
|
||||
getBitbucketFileFetchUrl,
|
||||
getBitbucketRequestOptions,
|
||||
readBitbucketIntegrationConfigs,
|
||||
} from '@backstage/integration';
|
||||
import fetch from 'cross-fetch';
|
||||
import parseGitUri from 'git-url-parse';
|
||||
import { NotFoundError } from '../errors';
|
||||
import { ReaderFactory, ReadTreeResponse, UrlReader } from './types';
|
||||
|
||||
export function getApiRequestOptions(
|
||||
provider: BitbucketIntegrationConfig,
|
||||
): RequestInit {
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
if (provider.token) {
|
||||
headers.Authorization = `Bearer ${provider.token}`;
|
||||
} else if (provider.username && provider.appPassword) {
|
||||
headers.Authorization = `Basic ${Buffer.from(
|
||||
`${provider.username}:${provider.appPassword}`,
|
||||
'utf8',
|
||||
).toString('base64')}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
// Converts for example
|
||||
// from: https://bitbucket.org/orgname/reponame/src/master/file.yaml
|
||||
// to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml
|
||||
export function getApiUrl(
|
||||
target: string,
|
||||
provider: BitbucketIntegrationConfig,
|
||||
): URL {
|
||||
try {
|
||||
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
|
||||
if (
|
||||
!owner ||
|
||||
!name ||
|
||||
(filepathtype !== 'browse' &&
|
||||
filepathtype !== 'raw' &&
|
||||
filepathtype !== 'src')
|
||||
) {
|
||||
throw new Error('Invalid Bitbucket URL or file path');
|
||||
}
|
||||
|
||||
const pathWithoutSlash = filepath.replace(/^\//, '');
|
||||
|
||||
if (provider.host === 'bitbucket.org') {
|
||||
if (!ref) {
|
||||
throw new Error('Invalid Bitbucket URL or file path');
|
||||
}
|
||||
return new URL(
|
||||
`${provider.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`,
|
||||
);
|
||||
}
|
||||
return new URL(
|
||||
`${provider.apiBaseUrl}/projects/${owner}/repos/${name}/raw/${pathWithoutSlash}?at=${ref}`,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A processor that adds the ability to read files from Bitbucket v1 and v2 APIs, such as
|
||||
* the one exposed by Bitbucket Cloud itself.
|
||||
@@ -116,9 +61,8 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
}
|
||||
|
||||
async read(url: string): Promise<Buffer> {
|
||||
const bitbucketUrl = getApiUrl(url, this.config);
|
||||
|
||||
const options = getApiRequestOptions(this.config);
|
||||
const bitbucketUrl = getBitbucketFileFetchUrl(url, this.config);
|
||||
const options = getBitbucketRequestOptions(this.config);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
|
||||
@@ -15,19 +15,12 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { GitHubIntegrationConfig } from '@backstage/integration';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
import fs from 'fs';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import path from 'path';
|
||||
import {
|
||||
getApiRequestOptions,
|
||||
getApiUrl,
|
||||
getRawRequestOptions,
|
||||
getRawUrl,
|
||||
GithubUrlReader,
|
||||
} from './GithubUrlReader';
|
||||
import { GithubUrlReader } from './GithubUrlReader';
|
||||
import { ReadTreeResponseFactory } from './tree';
|
||||
|
||||
const treeResponseFactory = ReadTreeResponseFactory.create({
|
||||
@@ -35,143 +28,6 @@ const treeResponseFactory = ReadTreeResponseFactory.create({
|
||||
});
|
||||
|
||||
describe('GithubUrlReader', () => {
|
||||
describe('getApiRequestOptions', () => {
|
||||
it('sets the correct API version', () => {
|
||||
const config: GitHubIntegrationConfig = { host: '', apiBaseUrl: '' };
|
||||
expect((getApiRequestOptions(config).headers as any).Accept).toEqual(
|
||||
'application/vnd.github.v3.raw',
|
||||
);
|
||||
});
|
||||
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: GitHubIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: GitHubIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getApiRequestOptions(withToken).headers as any).Authorization,
|
||||
).toEqual('token A');
|
||||
expect(
|
||||
(getApiRequestOptions(withoutToken).headers as any).Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRawRequestOptions', () => {
|
||||
it('inserts a token when needed', () => {
|
||||
const withToken: GitHubIntegrationConfig = {
|
||||
host: '',
|
||||
rawBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: GitHubIntegrationConfig = {
|
||||
host: '',
|
||||
rawBaseUrl: '',
|
||||
};
|
||||
expect(
|
||||
(getRawRequestOptions(withToken).headers as any).Authorization,
|
||||
).toEqual('token A');
|
||||
expect(
|
||||
(getRawRequestOptions(withoutToken).headers as any).Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApiUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: GitHubIntegrationConfig = { host: '', apiBaseUrl: '' };
|
||||
expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
|
||||
});
|
||||
|
||||
it('happy path for github', () => {
|
||||
const config: GitHubIntegrationConfig = {
|
||||
host: 'github.com',
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
};
|
||||
expect(
|
||||
getApiUrl(
|
||||
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
|
||||
),
|
||||
);
|
||||
expect(
|
||||
getApiUrl(
|
||||
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('happy path for ghe', () => {
|
||||
const config: GitHubIntegrationConfig = {
|
||||
host: 'ghe.mycompany.net',
|
||||
apiBaseUrl: 'https://ghe.mycompany.net/api/v3',
|
||||
};
|
||||
expect(
|
||||
getApiUrl(
|
||||
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://ghe.mycompany.net/api/v3/repos/a/b/contents/path/to/c.yaml?ref=branchname',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRawUrl', () => {
|
||||
it('rejects targets that do not look like URLs', () => {
|
||||
const config: GitHubIntegrationConfig = { host: '', apiBaseUrl: '' };
|
||||
expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
|
||||
});
|
||||
|
||||
it('happy path for github', () => {
|
||||
const config: GitHubIntegrationConfig = {
|
||||
host: 'github.com',
|
||||
rawBaseUrl: 'https://raw.githubusercontent.com',
|
||||
};
|
||||
expect(
|
||||
getRawUrl(
|
||||
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL(
|
||||
'https://raw.githubusercontent.com/a/b/branchname/path/to/c.yaml',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('happy path for ghe', () => {
|
||||
const config: GitHubIntegrationConfig = {
|
||||
host: 'ghe.mycompany.net',
|
||||
rawBaseUrl: 'https://ghe.mycompany.net/raw',
|
||||
};
|
||||
expect(
|
||||
getRawUrl(
|
||||
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
|
||||
config,
|
||||
),
|
||||
).toEqual(
|
||||
new URL('https://ghe.mycompany.net/raw/a/b/branchname/path/to/c.yaml'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('implementation', () => {
|
||||
it('rejects unknown targets', async () => {
|
||||
const processor = new GithubUrlReader(
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
import {
|
||||
GitHubIntegrationConfig,
|
||||
readGitHubIntegrationConfigs,
|
||||
getGitHubFileFetchUrl,
|
||||
getGitHubRequestOptions,
|
||||
} from '@backstage/integration';
|
||||
import fetch from 'cross-fetch';
|
||||
import parseGitUri from 'git-url-parse';
|
||||
@@ -30,92 +32,6 @@ import {
|
||||
UrlReader,
|
||||
} from './types';
|
||||
|
||||
export function getApiRequestOptions(
|
||||
provider: GitHubIntegrationConfig,
|
||||
): RequestInit {
|
||||
const headers: HeadersInit = {
|
||||
Accept: 'application/vnd.github.v3.raw',
|
||||
};
|
||||
|
||||
if (provider.token) {
|
||||
headers.Authorization = `token ${provider.token}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRawRequestOptions(
|
||||
provider: GitHubIntegrationConfig,
|
||||
): RequestInit {
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
if (provider.token) {
|
||||
headers.Authorization = `token ${provider.token}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
// Converts for example
|
||||
// from: https://github.com/a/b/blob/branchname/path/to/c.yaml
|
||||
// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname
|
||||
export function getApiUrl(
|
||||
target: string,
|
||||
provider: GitHubIntegrationConfig,
|
||||
): URL {
|
||||
try {
|
||||
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
|
||||
|
||||
if (
|
||||
!owner ||
|
||||
!name ||
|
||||
!ref ||
|
||||
(filepathtype !== 'blob' && filepathtype !== 'raw')
|
||||
) {
|
||||
throw new Error('Invalid GitHub URL or file path');
|
||||
}
|
||||
|
||||
const pathWithoutSlash = filepath.replace(/^\//, '');
|
||||
return new URL(
|
||||
`${provider.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Converts for example
|
||||
// from: https://github.com/a/b/blob/branchname/c.yaml
|
||||
// to: https://raw.githubusercontent.com/a/b/branchname/c.yaml
|
||||
export function getRawUrl(
|
||||
target: string,
|
||||
provider: GitHubIntegrationConfig,
|
||||
): URL {
|
||||
try {
|
||||
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
|
||||
|
||||
if (
|
||||
!owner ||
|
||||
!name ||
|
||||
!ref ||
|
||||
(filepathtype !== 'blob' && filepathtype !== 'raw')
|
||||
) {
|
||||
throw new Error('Invalid GitHub URL or file path');
|
||||
}
|
||||
|
||||
const pathWithoutSlash = filepath.replace(/^\//, '');
|
||||
return new URL(
|
||||
`${provider.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect URL: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A processor that adds the ability to read files from GitHub v3 APIs, such as
|
||||
* the one exposed by GitHub itself.
|
||||
@@ -144,14 +60,8 @@ export class GithubUrlReader implements UrlReader {
|
||||
}
|
||||
|
||||
async read(url: string): Promise<Buffer> {
|
||||
const useApi =
|
||||
this.config.apiBaseUrl && (this.config.token || !this.config.rawBaseUrl);
|
||||
const ghUrl = useApi
|
||||
? getApiUrl(url, this.config)
|
||||
: getRawUrl(url, this.config);
|
||||
const options = useApi
|
||||
? getApiRequestOptions(this.config)
|
||||
: getRawRequestOptions(this.config);
|
||||
const ghUrl = getGitHubFileFetchUrl(url, this.config);
|
||||
const options = getGitHubRequestOptions(this.config);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
@@ -196,7 +106,7 @@ export class GithubUrlReader implements UrlReader {
|
||||
new URL(
|
||||
`${protocol}://${resource}/${full_name}/archive/${ref}.tar.gz`,
|
||||
).toString(),
|
||||
getRawRequestOptions(this.config),
|
||||
getGitHubRequestOptions(this.config),
|
||||
);
|
||||
if (!response.ok) {
|
||||
const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
getGitLabFileFetchUrl,
|
||||
getGitLabRequestOptions,
|
||||
GitLabIntegrationConfig,
|
||||
readGitLabIntegrationConfigs,
|
||||
} from '@backstage/integration';
|
||||
@@ -37,20 +39,11 @@ export class GitlabUrlReader implements UrlReader {
|
||||
constructor(private readonly options: GitLabIntegrationConfig) {}
|
||||
|
||||
async read(url: string): Promise<Buffer> {
|
||||
// TODO(Rugvip): merged the old GitlabReaderProcessor in here and used
|
||||
// the existence of /~/blob/ to switch the logic. Don't know if this
|
||||
// makes sense and it might require some more work.
|
||||
let builtUrl: URL;
|
||||
if (url.includes('/-/blob/')) {
|
||||
const projectID = await this.getProjectID(url);
|
||||
builtUrl = this.buildProjectUrl(url, projectID);
|
||||
} else {
|
||||
builtUrl = this.buildRawUrl(url);
|
||||
}
|
||||
const builtUrl = await getGitLabFileFetchUrl(url, this.options);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(builtUrl.toString(), this.getRequestOptions());
|
||||
response = await fetch(builtUrl, getGitLabRequestOptions(this.options));
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to read ${url}, ${e}`);
|
||||
}
|
||||
@@ -70,109 +63,6 @@ export class GitlabUrlReader implements UrlReader {
|
||||
throw new Error('GitlabUrlReader does not implement readTree');
|
||||
}
|
||||
|
||||
// Converts
|
||||
// from: https://gitlab.example.com/a/b/blob/master/c.yaml
|
||||
// to: https://gitlab.example.com/a/b/raw/master/c.yaml
|
||||
private buildRawUrl(target: string): URL {
|
||||
try {
|
||||
const url = new URL(target);
|
||||
|
||||
const [
|
||||
empty,
|
||||
userOrOrg,
|
||||
repoName,
|
||||
blobKeyword,
|
||||
...restOfPath
|
||||
] = url.pathname.split('/');
|
||||
|
||||
if (
|
||||
empty !== '' ||
|
||||
userOrOrg === '' ||
|
||||
repoName === '' ||
|
||||
blobKeyword !== 'blob' ||
|
||||
!restOfPath.join('/').match(/\.yaml$/)
|
||||
) {
|
||||
throw new Error('Wrong GitLab URL');
|
||||
}
|
||||
|
||||
// Replace 'blob' with 'raw'
|
||||
url.pathname = [empty, userOrOrg, repoName, 'raw', ...restOfPath].join(
|
||||
'/',
|
||||
);
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect url: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// convert https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
|
||||
// to https://gitlab.com/api/v4/projects/<PROJECTID>/repository/files/filepath?ref=branch
|
||||
private buildProjectUrl(target: string, projectID: Number): URL {
|
||||
try {
|
||||
const url = new URL(target);
|
||||
|
||||
const branchAndFilePath = url.pathname.split('/-/blob/')[1];
|
||||
|
||||
const [branch, ...filePath] = branchAndFilePath.split('/');
|
||||
|
||||
url.pathname = [
|
||||
'/api/v4/projects',
|
||||
projectID,
|
||||
'repository/files',
|
||||
encodeURIComponent(filePath.join('/')),
|
||||
'raw',
|
||||
].join('/');
|
||||
url.search = `?ref=${branch}`;
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect url: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getProjectID(target: string): Promise<Number> {
|
||||
const url = new URL(target);
|
||||
|
||||
if (
|
||||
// absPaths to gitlab files should contain /-/blob
|
||||
// ex: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
|
||||
!url.pathname.match(/\/\-\/blob\//)
|
||||
) {
|
||||
throw new Error('Please provide full path to yaml file from Gitlab');
|
||||
}
|
||||
try {
|
||||
const repo = url.pathname.split('/-/blob/')[0];
|
||||
|
||||
// Find ProjectID from url
|
||||
// convert 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath'
|
||||
// to 'https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo'
|
||||
const repoIDLookup = new URL(
|
||||
`${url.protocol + url.hostname}/api/v4/projects/${encodeURIComponent(
|
||||
repo.replace(/^\//, ''),
|
||||
)}`,
|
||||
);
|
||||
const response = await fetch(
|
||||
repoIDLookup.toString(),
|
||||
this.getRequestOptions(),
|
||||
);
|
||||
const projectIDJson = await response.json();
|
||||
const projectID: Number = projectIDJson.id;
|
||||
|
||||
return projectID;
|
||||
} catch (e) {
|
||||
throw new Error(`Could not get GitLab ProjectID for: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
private getRequestOptions(): RequestInit {
|
||||
return {
|
||||
headers: {
|
||||
['PRIVATE-TOKEN']: this.options.token ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
toString() {
|
||||
const { host, token } = this.options;
|
||||
return `gitlab{host=${host},authed=${Boolean(token)}}`;
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
readHttpsSettings,
|
||||
} from './config';
|
||||
import { createHttpServer, createHttpsServer } from './hostFactory';
|
||||
import { metricsHandler } from './metrics';
|
||||
|
||||
export const DEFAULT_PORT = 7000;
|
||||
// '' is express default, which listens to all interfaces
|
||||
@@ -66,7 +65,6 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
private corsOptions: cors.CorsOptions | undefined;
|
||||
private cspOptions: Record<string, string[] | false> | undefined;
|
||||
private httpsSettings: HttpsSettings | undefined;
|
||||
private enableMetrics: boolean = true;
|
||||
private routers: [string, Router][];
|
||||
// Reference to the module where builder is created - needed for hot module
|
||||
// reloading
|
||||
@@ -109,9 +107,6 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
this.httpsSettings = httpsSettings;
|
||||
}
|
||||
|
||||
// For now, configuration of metrics is a simple boolean and active by default
|
||||
this.enableMetrics = backendConfig.getOptionalBoolean('metrics') !== false;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -166,9 +161,6 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
app.use(cors(corsOptions));
|
||||
}
|
||||
app.use(compression());
|
||||
if (this.enableMetrics) {
|
||||
app.use(metricsHandler());
|
||||
}
|
||||
app.use(requestLoggingHandler());
|
||||
for (const [root, route] of this.routers) {
|
||||
app.use(root, route);
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { normalizePath } from './metrics';
|
||||
|
||||
describe('normalizePath', () => {
|
||||
it('should normalize /path to /path', async () => {
|
||||
const path = normalizePath({ url: 'http://server/path' });
|
||||
|
||||
expect(path).toBe('/path');
|
||||
});
|
||||
|
||||
it('should normalize /path/test to /path', async () => {
|
||||
const path = normalizePath({ url: 'http://server/path/test' });
|
||||
|
||||
expect(path).toBe('/path');
|
||||
});
|
||||
|
||||
it('should normalize /api/plugin-name/test to /api/plugin-name', async () => {
|
||||
const path = normalizePath({ url: 'http://server/api/plugin-name/test' });
|
||||
|
||||
expect(path).toBe('/api/plugin-name');
|
||||
});
|
||||
});
|
||||
@@ -3,11 +3,20 @@
|
||||
"version": "0.2.5",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "12 || 14"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/backend"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli backend:build",
|
||||
"build-image": "backstage-cli backend:build-image --build --tag example-backend",
|
||||
@@ -29,7 +38,6 @@
|
||||
"@backstage/plugin-proxy-backend": "^0.2.2",
|
||||
"@backstage/plugin-rollbar-backend": "^0.1.4",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.3.3",
|
||||
"@backstage/plugin-sentry-backend": "^0.1.3",
|
||||
"@backstage/plugin-techdocs-backend": "^0.3.1",
|
||||
"@gitbeaker/node": "^25.2.0",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
@@ -50,5 +58,8 @@
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5",
|
||||
"@types/helmet": "^0.0.48"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
import Router from 'express-promise-router';
|
||||
import {
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
getRootLogger,
|
||||
useHotMemoize,
|
||||
loadBackendConfig,
|
||||
notFoundHandler,
|
||||
SingleConnectionDatabaseManager,
|
||||
SingleHostDiscovery,
|
||||
UrlReaders,
|
||||
useHotMemoize,
|
||||
} from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import healthcheck from './plugins/healthcheck';
|
||||
@@ -40,7 +40,6 @@ import catalog from './plugins/catalog';
|
||||
import kubernetes from './plugins/kubernetes';
|
||||
import rollbar from './plugins/rollbar';
|
||||
import scaffolder from './plugins/scaffolder';
|
||||
import sentry from './plugins/sentry';
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import graphql from './plugins/graphql';
|
||||
@@ -76,7 +75,6 @@ async function main() {
|
||||
const authEnv = useHotMemoize(module, () => createEnv('auth'));
|
||||
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
|
||||
const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar'));
|
||||
const sentryEnv = useHotMemoize(module, () => createEnv('sentry'));
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes'));
|
||||
const graphqlEnv = useHotMemoize(module, () => createEnv('graphql'));
|
||||
@@ -86,7 +84,6 @@ async function main() {
|
||||
apiRouter.use('/catalog', await catalog(catalogEnv));
|
||||
apiRouter.use('/rollbar', await rollbar(rollbarEnv));
|
||||
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
|
||||
apiRouter.use('/sentry', await sentry(sentryEnv));
|
||||
apiRouter.use('/auth', await auth(authEnv));
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/catalog-client"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli build",
|
||||
"lint": "backstage-cli lint",
|
||||
|
||||
@@ -10,3 +10,5 @@ spec:
|
||||
- ./apis/spotify-api.yaml
|
||||
- ./apis/streetlights-api.yaml
|
||||
- ./apis/swapi-graphql.yaml
|
||||
- ./apis/wayback-archive-api.yaml
|
||||
- ./apis/wayback-search-api.yaml
|
||||
|
||||
@@ -14,3 +14,5 @@ spec:
|
||||
- ./components/playback-lib-component.yaml
|
||||
- ./components/www-artist-component.yaml
|
||||
- ./components/shuffle-api-component.yaml
|
||||
- ./components/wayback-archive-component.yaml
|
||||
- ./components/wayback-search-component.yaml
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: API
|
||||
metadata:
|
||||
name: wayback-archive
|
||||
description: Archive API for the wayback machine
|
||||
spec:
|
||||
type: openapi
|
||||
lifecycle: production
|
||||
owner: archive@example.com
|
||||
definition:
|
||||
$text: https://github.com/APIs-guru/openapi-directory/blob/master/APIs/archive.org/wayback/1.0.0/openapi.yaml
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: API
|
||||
metadata:
|
||||
name: wayback-search
|
||||
description: Search API for the wayback machine
|
||||
spec:
|
||||
type: openapi
|
||||
lifecycle: production
|
||||
owner: archive@example.com
|
||||
definition:
|
||||
$text: https://github.com/APIs-guru/openapi-directory/blob/master/APIs/archive.org/search/1.0.0/openapi.yaml
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: wayback-archive
|
||||
description: Archive of the wayback machine
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: production
|
||||
owner: archive@example.com
|
||||
providesApis:
|
||||
- wayback-archive
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: wayback-search
|
||||
description: Search of the wayback machine
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: production
|
||||
owner: archive@example.com
|
||||
providesApis:
|
||||
- wayback-search
|
||||
consumesApis:
|
||||
- wayback-archive
|
||||
@@ -6,6 +6,4 @@ metadata:
|
||||
spec:
|
||||
type: sub-department
|
||||
parent: infrastructure
|
||||
ancestors: [infrastructure, acme-corp]
|
||||
children: [team-a, team-b]
|
||||
descendants: [team-a, team-b]
|
||||
|
||||
@@ -6,6 +6,4 @@ metadata:
|
||||
spec:
|
||||
type: sub-department
|
||||
parent: infrastructure
|
||||
ancestors: [infrastructure, acme-corp]
|
||||
children: [team-c, team-d]
|
||||
descendants: [team-c, team-d]
|
||||
|
||||
@@ -6,6 +6,4 @@ metadata:
|
||||
spec:
|
||||
type: department
|
||||
parent: acme-corp
|
||||
ancestors: [acme-corp]
|
||||
children: [backstage, boxoffice]
|
||||
descendants: [backstage, boxoffice, team-a, team-b, team-c, team-d]
|
||||
|
||||
@@ -5,10 +5,7 @@ metadata:
|
||||
description: The acme-corp organization
|
||||
spec:
|
||||
type: organization
|
||||
ancestors: []
|
||||
children: [infrastructure]
|
||||
descendants:
|
||||
[infrastructure, backstage, boxoffice, team-a, team-b, team-c, team-d]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Location
|
||||
|
||||
@@ -6,9 +6,7 @@ metadata:
|
||||
spec:
|
||||
type: team
|
||||
parent: backstage
|
||||
ancestors: [backstage, infrastructure, acme-corp]
|
||||
children: []
|
||||
descendants: []
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: User
|
||||
|
||||
@@ -6,9 +6,7 @@ metadata:
|
||||
spec:
|
||||
type: team
|
||||
parent: backstage
|
||||
ancestors: [backstage, infrastructure, acme-corp]
|
||||
children: []
|
||||
descendants: []
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: User
|
||||
|
||||
@@ -6,9 +6,7 @@ metadata:
|
||||
spec:
|
||||
type: team
|
||||
parent: boxoffice
|
||||
ancestors: [boxoffice, infrastructure, acme-corp]
|
||||
children: []
|
||||
descendants: []
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: User
|
||||
|
||||
@@ -6,9 +6,7 @@ metadata:
|
||||
spec:
|
||||
type: team
|
||||
parent: boxoffice
|
||||
ancestors: [boxoffice, infrastructure, acme-corp]
|
||||
children: []
|
||||
descendants: []
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: User
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/catalog-model"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli build",
|
||||
"lint": "backstage-cli lint",
|
||||
|
||||
@@ -34,9 +34,7 @@ describe('GroupV1alpha1Validator', () => {
|
||||
spec: {
|
||||
type: 'squad',
|
||||
parent: 'group-a',
|
||||
ancestors: ['group-a', 'global-synergies', 'acme-corp'],
|
||||
children: ['child-a', 'child-b'],
|
||||
descendants: ['desc-a', 'desc-b'],
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -85,26 +83,6 @@ describe('GroupV1alpha1Validator', () => {
|
||||
await expect(validator.check(entity)).rejects.toThrow(/parent/);
|
||||
});
|
||||
|
||||
it('rejects missing ancestors', async () => {
|
||||
delete (entity as any).spec.ancestors;
|
||||
await expect(validator.check(entity)).rejects.toThrow(/ancestor/);
|
||||
});
|
||||
|
||||
it('rejects empty ancestors', async () => {
|
||||
(entity as any).spec.ancestors = [''];
|
||||
await expect(validator.check(entity)).rejects.toThrow(/ancestor/);
|
||||
});
|
||||
|
||||
it('rejects undefined ancestors', async () => {
|
||||
(entity as any).spec.ancestors = [undefined];
|
||||
await expect(validator.check(entity)).rejects.toThrow(/ancestor/);
|
||||
});
|
||||
|
||||
it('accepts no ancestors', async () => {
|
||||
(entity as any).spec.ancestors = [];
|
||||
await expect(validator.check(entity)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('rejects missing children', async () => {
|
||||
delete (entity as any).spec.children;
|
||||
await expect(validator.check(entity)).rejects.toThrow(/children/);
|
||||
@@ -124,24 +102,4 @@ describe('GroupV1alpha1Validator', () => {
|
||||
(entity as any).spec.children = [];
|
||||
await expect(validator.check(entity)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('rejects missing descendants', async () => {
|
||||
delete (entity as any).spec.descendants;
|
||||
await expect(validator.check(entity)).rejects.toThrow(/descendants/);
|
||||
});
|
||||
|
||||
it('rejects empty descendants', async () => {
|
||||
(entity as any).spec.descendants = [''];
|
||||
await expect(validator.check(entity)).rejects.toThrow(/descendants/);
|
||||
});
|
||||
|
||||
it('rejects undefined descendants', async () => {
|
||||
(entity as any).spec.descendants = [undefined];
|
||||
await expect(validator.check(entity)).rejects.toThrow(/descendants/);
|
||||
});
|
||||
|
||||
it('accepts no descendants', async () => {
|
||||
(entity as any).spec.descendants = [];
|
||||
await expect(validator.check(entity)).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,21 +32,11 @@ const schema = yup.object<Partial<GroupEntityV1alpha1>>({
|
||||
// one element and there is no simple workaround -_-
|
||||
// the cast is there to convince typescript that the array itself is
|
||||
// required without using .required()
|
||||
ancestors: yup.array(yup.string().required()).test({
|
||||
name: 'isDefined',
|
||||
message: 'ancestors must be defined',
|
||||
test: v => Boolean(v),
|
||||
}) as yup.ArraySchema<string, object>,
|
||||
children: yup.array(yup.string().required()).test({
|
||||
name: 'isDefined',
|
||||
message: 'children must be defined',
|
||||
test: v => Boolean(v),
|
||||
}) as yup.ArraySchema<string, object>,
|
||||
descendants: yup.array(yup.string().required()).test({
|
||||
name: 'isDefined',
|
||||
message: 'descendants must be defined',
|
||||
test: v => Boolean(v),
|
||||
}) as yup.ArraySchema<string, object>,
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
@@ -57,23 +47,7 @@ export interface GroupEntityV1alpha1 extends Entity {
|
||||
spec: {
|
||||
type: string;
|
||||
parent?: string;
|
||||
/**
|
||||
* @deprecated This field will disappear on Dec 6th, 2020. Please remove
|
||||
* any consuming code. Producers can stop producing this field
|
||||
* before that date, as long as the catalog backend uses the
|
||||
* BuiltinKindsEntityProcessor which inserts the fields in the
|
||||
* mean time.
|
||||
*/
|
||||
ancestors: string[];
|
||||
children: string[];
|
||||
/**
|
||||
* @deprecated This field will disappear on Dec 6th, 2020. Please remove
|
||||
* any consuming code. Producers can stop producing this field
|
||||
* before that date, as long as the catalog backend uses the
|
||||
* BuiltinKindsEntityProcessor which inserts the fields in the
|
||||
* mean time.
|
||||
*/
|
||||
descendants: string[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ This package provides a CLI for developing Backstage plugins and apps.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the package via npm or yarn:
|
||||
Install the package via npm or Yarn:
|
||||
|
||||
```sh
|
||||
$ npm install --save @backstage/cli
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"@sucrase/webpack-loader": "^2.0.0",
|
||||
"@svgr/plugin-jsx": "5.4.x",
|
||||
"@svgr/plugin-svgo": "5.4.x",
|
||||
"@svgr/rollup": "5.4.x",
|
||||
"@svgr/rollup": "5.5.x",
|
||||
"@svgr/webpack": "5.4.x",
|
||||
"@types/start-server-webpack-plugin": "^2.2.0",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
|
||||
@@ -14,26 +14,59 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import os from 'os';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import tar, { CreateOptions } from 'tar';
|
||||
import { Command } from 'commander';
|
||||
import { createDistWorkspace } from '../../lib/packager';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
|
||||
import { buildPackage, Output } from '../../lib/builder';
|
||||
|
||||
const PKG_PATH = 'package.json';
|
||||
const TARGET_DIR = 'dist-workspace';
|
||||
const BUNDLE_FILE = 'bundle.tar.gz';
|
||||
const SKELETON_FILE = 'skeleton.tar.gz';
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
const targetDir = paths.resolveTarget(TARGET_DIR);
|
||||
const pkgPath = paths.resolveTarget(PKG_PATH);
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
const targetDir = paths.resolveTarget('dist');
|
||||
const pkg = await fs.readJson(paths.resolveTarget('package.json'));
|
||||
|
||||
await fs.remove(targetDir);
|
||||
await fs.mkdir(targetDir);
|
||||
await createDistWorkspace([pkg.name], {
|
||||
targetDir: targetDir,
|
||||
buildDependencies: Boolean(cmd.build),
|
||||
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
|
||||
skeleton: 'skeleton.tar',
|
||||
});
|
||||
// We build the target package without generating type declarations.
|
||||
await buildPackage({ outputs: new Set([Output.cjs]) });
|
||||
|
||||
const tmpDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-bundle'));
|
||||
try {
|
||||
await createDistWorkspace([pkg.name], {
|
||||
targetDir: tmpDir,
|
||||
buildDependencies: Boolean(cmd.build),
|
||||
buildExcludes: [pkg.name],
|
||||
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
|
||||
skeleton: SKELETON_FILE,
|
||||
});
|
||||
|
||||
// We built the target backend package using the regular build process, but the result of
|
||||
// that has now been packed into the dist workspace, so clean up the dist dir.
|
||||
await fs.remove(targetDir);
|
||||
await fs.mkdir(targetDir);
|
||||
|
||||
// Move out skeleton.tar.gz before we create the main bundle, no point having that included up twice.
|
||||
await fs.move(
|
||||
resolvePath(tmpDir, SKELETON_FILE),
|
||||
resolvePath(targetDir, SKELETON_FILE),
|
||||
);
|
||||
|
||||
// Create main bundle.tar.gz, with some tweaks to make it more likely hit Docker build cache.
|
||||
await tar.create(
|
||||
{
|
||||
file: resolvePath(targetDir, BUNDLE_FILE),
|
||||
cwd: tmpDir,
|
||||
portable: true,
|
||||
noMtime: true,
|
||||
gzip: true,
|
||||
} as CreateOptions & { noMtime: boolean },
|
||||
[''],
|
||||
);
|
||||
} finally {
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,9 +87,9 @@ export function registerCommands(program: CommanderStatic) {
|
||||
'Create plugin with the backend dependencies as default',
|
||||
)
|
||||
.description('Creates a new plugin in the current repository')
|
||||
.option('--scope <scope>', 'NPM scope')
|
||||
.option('--npm-registry <URL>', 'NPM registry URL')
|
||||
.option('--no-private', 'Public NPM Package')
|
||||
.option('--scope <scope>', 'npm scope')
|
||||
.option('--npm-registry <URL>', 'npm registry URL')
|
||||
.option('--no-private', 'Public npm package')
|
||||
.action(
|
||||
lazy(() => import('./create-plugin/createPlugin').then(m => m.default)),
|
||||
);
|
||||
|
||||
@@ -35,7 +35,12 @@ export const transforms = (options: TransformOptions): Transforms => {
|
||||
const extraTransforms = isDev ? ['react-hot-loader'] : [];
|
||||
|
||||
const transformExcludeCondition = {
|
||||
and: [/node_modules/, { not: externalTransforms }],
|
||||
or: [
|
||||
// This makes sure we don't transform node_modules inside any of the local monorepo packages
|
||||
/node_modules.*node_modules/,
|
||||
// This excludes the local monorepo packages from the excludes, meaning they will be transformed
|
||||
{ and: [/node_modules/, { not: externalTransforms }] },
|
||||
],
|
||||
};
|
||||
|
||||
const loaders = [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user