Merge branch 'master' into ryanv/product-insights-intervals

This commit is contained in:
Ryan Vazquez
2020-12-09 15:50:17 -05:00
336 changed files with 9217 additions and 4434 deletions
+5
View File
@@ -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.
+65
View File
@@ -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
+5
View File
@@ -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/`.
+5
View File
@@ -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.
+5
View File
@@ -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`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Batch the fetching of relations
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
fix react-hooks/exhaustive-deps error
+13
View File
@@ -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"
]
```
+6
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Add the basics of cross-integration concerns
+11
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
Fix React warning of descendant paragraph tag
@@ -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.
+9
View File
@@ -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`.
+13
View File
@@ -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.
+136
View File
@@ -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} />;
+ }
+};
```
+8
View File
@@ -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.
+6
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/theme': patch
---
Add a little more padding in dense tables
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
Export the `defaultConfigLoader` implementation
+3 -2
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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 |
+7
View File
@@ -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 }}
+1
View File
@@ -250,3 +250,4 @@ auth:
gitlabToken: g
newRelicRestApiKey: r
travisciAuthToken: fake-travis-ci-auth-token
pagerdutyToken: h
+56
View File
@@ -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:
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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.
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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) -
+1 -1
View File
@@ -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,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: Spotifys 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 youre 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 engineers solution to taming cloud costs
author: Janisa Anandamohan
author: Janisa Anandamohan, Spotify
authorURL: https://twitter.com/janisa_a
---
+9
View File
@@ -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 &nbsp; at Spotify</a>
</p>
<p className="copyright">{this.props.config.copyright}</p>
</footer>
);
+12
View File
@@ -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
+2 -2
View File
@@ -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"
+1 -9
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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'
+4
View File
@@ -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} />;
}
+1
View File
@@ -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';
-2
View File
@@ -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');
});
});
+14 -3
View File
@@ -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"
]
}
+2 -5
View File
@@ -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));
+9
View File
@@ -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
+9
View File
@@ -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[];
};
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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",
+47 -14
View File
@@ -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);
}
};
+3 -3
View File
@@ -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)),
);
+6 -1
View File
@@ -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