Merge remote-tracking branch 'backstage/master' into erikengervall/plugin-release-manager-as-a-service
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/create-app': patch
|
||||
'@backstage/integration': patch
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
'@backstage/plugin-github-actions': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
The default `@octokit/rest` dependency was bumped to `"^18.5.3"`.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
Fix some transitive dependency warnings in yarn
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Fetch relations in batches in `useRelatedEntities`
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
---
|
||||
|
||||
Bump react-hook-form version to be the same for the entire project.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Expose `BitbucketRepositoryParser` introduced in [#5295](https://github.com/backstage/backstage/pull/5295)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Added lax option to backstage-cli app:build command
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Fix GithubPR built-in action `credentialsProvider.getCredentials` URL.
|
||||
Adding Documentation for GitHub PR built-in action.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
---
|
||||
|
||||
Adds @backstage/plugin-kubernetes-common library to share types between kubernetes frontend and backend.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
bump `jsonschema` from 1.2.7 to 1.4.0
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Add support for non-organization accounts in GitHub Discovery
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Close eventSource upon completion of a scaffolder task
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Make git config optional for techdocs feedback links
|
||||
@@ -111,6 +111,7 @@ Usage: backstage-cli app:build
|
||||
|
||||
Options:
|
||||
--stats Write bundle stats to output directory
|
||||
--lax Do not require environment variables to be set
|
||||
--config <path> Config files to load instead of app-config.yaml (default: [])
|
||||
-h, --help display help for command
|
||||
```
|
||||
@@ -486,6 +487,7 @@ Usage: backstage-cli config:print [options]
|
||||
|
||||
Options:
|
||||
--package <name> Only load config schema that applies to the given package
|
||||
--lax Do not require environment variables to be set
|
||||
--frontend Print only the frontend configuration
|
||||
--with-secrets Include secrets in the printed configuration
|
||||
--format <format> Format to print the configuration in, either json or yaml [yaml]
|
||||
@@ -506,6 +508,7 @@ Usage: backstage-cli config:check [options]
|
||||
|
||||
Options:
|
||||
--package <name> Only load config schema that applies to the given package
|
||||
--lax Do not require environment variables to be set
|
||||
--config <path> Config files to load instead of app-config.yaml (default: [])
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
@@ -8,44 +8,36 @@ The Kubernetes feature is a plugin to Backstage, and it is exposed as a tab when
|
||||
viewing entities in the software catalog.
|
||||
|
||||
If you haven't setup Backstage already, read the
|
||||
[Getting Started](../../getting-started/index.md).
|
||||
[Getting Started](../../getting-started/index.md) guide.
|
||||
|
||||
## Adding the Kubernetes frontend plugin
|
||||
|
||||
The first step is to add the frontend Kubernetes plugin to your Backstage
|
||||
application. Navigate to your new Backstage application directory. And then to
|
||||
The first step is to add the Kubernetes frontend plugin to your Backstage
|
||||
application. Navigate to your new Backstage application directory, and then to
|
||||
your `packages/app` directory, and install the `@backstage/plugin-kubernetes`
|
||||
package.
|
||||
|
||||
```bash
|
||||
cd my-backstage-app/
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-kubernetes
|
||||
```
|
||||
|
||||
Once the package has been installed, you need to import the plugin in your app
|
||||
by adding the "Kubernetes" tab to the catalog entity page. In
|
||||
`packages/app/src/components/catalog/EntityPage.tsx`, you'll add a router to get
|
||||
to the tab, and add the tab itself.
|
||||
|
||||
`EntityPage.tsx`:
|
||||
by adding the "Kubernetes" tab to the respective catalog pages.
|
||||
|
||||
```tsx
|
||||
import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes';
|
||||
// In packages/app/src/components/catalog/EntityPage.tsx
|
||||
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
|
||||
|
||||
// ...
|
||||
|
||||
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
// ...
|
||||
<EntityPageLayout.Content
|
||||
path="/kubernetes/*"
|
||||
title="Kubernetes"
|
||||
element={<KubernetesRouter entity={entity} />}
|
||||
/>
|
||||
// ...
|
||||
</EntityPageLayout>
|
||||
);
|
||||
// You can add the tab to any number of pages, the service page is shown as an
|
||||
// example here
|
||||
const serviceEntityPage = (
|
||||
<EntityLayout>
|
||||
{/* other tabs... */}
|
||||
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
|
||||
<EntityKubernetesContent />
|
||||
</EntityLayout.Route>
|
||||
```
|
||||
|
||||
That's it! But now, we need the Kubernetes Backend plugin for the frontend to
|
||||
@@ -57,17 +49,16 @@ Navigate to `packages/backend` of your Backstage app, and install the
|
||||
`@backstage/plugin-kubernetes-backend` package.
|
||||
|
||||
```bash
|
||||
cd my-backstage-app/
|
||||
# From your Backstage root directory
|
||||
cd packages/backend
|
||||
yarn add @backstage/plugin-kubernetes-backend
|
||||
```
|
||||
|
||||
Create a file called `kubernetes.ts` inside `packages/backend/src/plugins/` and
|
||||
add the following
|
||||
|
||||
`kubernetes.ts`:
|
||||
add the following:
|
||||
|
||||
```typescript
|
||||
// In packages/backend/src/plugins/kubernetes.ts
|
||||
import { createRouter } from '@backstage/plugin-kubernetes-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
@@ -83,18 +74,15 @@ And import the plugin to `packages/backend/src/index.ts`. There are three lines
|
||||
of code you'll need to add, and they should be added near similar code in your
|
||||
existing Backstage backend.
|
||||
|
||||
`index.ts`:
|
||||
|
||||
```typescript
|
||||
// In packages/backend/src/index.ts
|
||||
import kubernetes from './plugins/kubernetes';
|
||||
|
||||
// ...
|
||||
|
||||
const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes'));
|
||||
|
||||
// ...
|
||||
|
||||
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
|
||||
async function main() {
|
||||
// ...
|
||||
const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes'));
|
||||
// ...
|
||||
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
|
||||
```
|
||||
|
||||
That's it! The Kubernetes frontend and backend have now been added to your
|
||||
|
||||
@@ -37,6 +37,13 @@ The locations added through static configuration cannot be removed through the
|
||||
catalog locations API. To remove these locations, you must remove them from the
|
||||
configuration.
|
||||
|
||||
Syntax errors or other types of errors present in `catalog-info.yaml` files will
|
||||
be logged for investigation. Errors do not cause processing to abort.
|
||||
|
||||
When multiple `catalog-info.yaml` files with the same `metadata.name` property
|
||||
are discovered, one will be processed and all others will be skipped. This
|
||||
action is logged for further investigation.
|
||||
|
||||
### Integration Processors
|
||||
|
||||
Integrations may simply provide a mechanism to handle `url` location type for an
|
||||
@@ -111,8 +118,7 @@ catalog:
|
||||
> deleting entities will not work in this mode.**
|
||||
|
||||
A common use case for this configuration is when organizations have a remote
|
||||
source that should be mirrored into backstage. If we want backstage to be a
|
||||
mirror of this remote source we cannot allow users to also register entities
|
||||
with e.g.
|
||||
source that should be mirrored into Backstage. To make Backstage a mirror of
|
||||
this remote source, users cannot also register new entities with e.g. the
|
||||
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
|
||||
plugin.
|
||||
|
||||
@@ -18,6 +18,7 @@ The catalog frontend plugin should be installed in your `app` package, which is
|
||||
created as a part of `@backstage/create-app`. To install the package, run:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-catalog
|
||||
```
|
||||
@@ -103,6 +104,7 @@ The catalog backend should be installed in your `backend` package, which is
|
||||
created as a part of `@backstage/create-app`. To install the package, run:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/backend
|
||||
yarn add @backstage/plugin-catalog-backend
|
||||
```
|
||||
|
||||
@@ -20,6 +20,7 @@ The scaffolder frontend plugin should be installed in your `app` package, which
|
||||
is created as a part of `@backstage/create-app`. To install the package, run:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-scaffolder
|
||||
```
|
||||
@@ -57,6 +58,7 @@ The scaffolder backend should be installed in your `backend` package, which is
|
||||
created as a part of `@backstage/create-app`. To install the package, run:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/backend
|
||||
yarn add @backstage/plugin-scaffolder-backend
|
||||
```
|
||||
@@ -262,6 +264,6 @@ library. By default it will use the
|
||||
[spotify/backstage-cookiecutter](https://github.com/backstage/backstage/blob/37e35b910afc7d1270855aed0ec4718aba366c91/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile)
|
||||
docker image.
|
||||
|
||||
If you are running backstage from a Docker container and you want to avoid
|
||||
If you are running Backstage from a Docker container and you want to avoid
|
||||
calling a container inside a container, you can set up Cookiecutter in your own
|
||||
image, this will use the local installation instead.
|
||||
|
||||
@@ -148,6 +148,23 @@ jobs:
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-python@v2
|
||||
|
||||
# the 2 steps below can be removed if you aren't using plantuml in your documentation
|
||||
- name: setup java
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
- name: download, validate, install plantuml and its dependencies
|
||||
run: |
|
||||
curl -o plantuml.jar -L http://sourceforge.net/projects/plantuml/files/plantuml.1.2021.4.jar/download
|
||||
echo "be498123d20eaea95a94b174d770ef94adfdca18 plantuml.jar" | sha1sum -c -
|
||||
mv plantuml.jar /opt/plantuml.jar
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
echo $'#!/bin/sh\n\njava -jar '/opt/plantuml.jar' ${@}' >> "$HOME/.local/bin/plantuml"
|
||||
chmod +x "$HOME/.local/bin/plantuml"
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
sudo apt-get install -y graphviz
|
||||
|
||||
- name: Install techdocs-cli
|
||||
run: sudo npm install -g @techdocs/cli
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Navigate to your new Backstage application directory. And then to your
|
||||
`packages/app` directory, and install the `@backstage/plugin-techdocs` package.
|
||||
|
||||
```bash
|
||||
cd my-backstage-app/
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-techdocs
|
||||
```
|
||||
@@ -54,7 +54,7 @@ Navigate to `packages/backend` of your Backstage app, and install the
|
||||
`@backstage/plugin-techdocs-backend` package.
|
||||
|
||||
```bash
|
||||
cd my-backstage-app/
|
||||
# From your Backstage root directory
|
||||
cd packages/backend
|
||||
yarn add @backstage/plugin-techdocs-backend
|
||||
```
|
||||
|
||||
@@ -135,3 +135,31 @@ const themeOptions = createThemeOptions({
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Logo
|
||||
|
||||
In addition to a custom theme, you can also customize the logo displayed at the
|
||||
far top left of the site.
|
||||
|
||||
In your frontend app, locate `src/components/Root/` folder. You'll find two
|
||||
components:
|
||||
|
||||
- `LogoFull.tsx` - A larger logo used when the Sidebar navigation is opened.
|
||||
- `LogoIcon.tsx` - A smaller logo used when the sidebar navigation is closed.
|
||||
|
||||
To replace the images, you can simply replace the relevant code in those
|
||||
components with raw SVG definitions.
|
||||
|
||||
You can also use another web image format such as PNG by importing it. To do
|
||||
this, place your new image into a new subdirectory such as
|
||||
`src/components/Root/logo/my-company-logo.png`, and then add this code:
|
||||
|
||||
```jsx
|
||||
import MyCustomLogoFull from './logo/my-company-logo.png';
|
||||
|
||||
//...
|
||||
|
||||
const LogoFull = () => {
|
||||
return <img src={MyCustomLogoFull} />;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: integrating-plugin-into-service-catalog
|
||||
title: Integrate into the Service Catalog
|
||||
description: Documentation on How to integrate plugin into service catalog
|
||||
description: How to integrate a plugin into service catalog
|
||||
---
|
||||
|
||||
> This is an advanced use case and currently is an experimental feature. Expect
|
||||
|
||||
@@ -17,11 +17,11 @@ switch between database backends.
|
||||
|
||||
## Install PostgreSQL
|
||||
|
||||
First, swap out SQLite for PostgreSQL in your `backend` package:
|
||||
First, add PostgreSQL to your `backend` package:
|
||||
|
||||
```shell
|
||||
# From your Backstage root directory
|
||||
cd packages/backend
|
||||
yarn remove sqlite3
|
||||
yarn add pg
|
||||
```
|
||||
|
||||
@@ -46,7 +46,6 @@ backend:
|
||||
+ #ssl: require # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require)
|
||||
+ #ca: # if you have a CA file and want to verify it you can uncomment this section
|
||||
+ #$file: <file-path>/ca/server.crt
|
||||
|
||||
```
|
||||
|
||||
If you have an `app-config.local.yaml` for local development, a similar update
|
||||
|
||||
+20
-12
@@ -1,3 +1,9 @@
|
||||
# Backstage Documentation
|
||||
|
||||
This folder holds the service and configuration that runs Backstage's documentation hosted at https://backstage.io.
|
||||
|
||||
It pulls content in from the [root `/docs`](../docs/) folder and builds it into the resulting HTML web site.
|
||||
|
||||
This website was created with [Docusaurus](https://docusaurus.io/).
|
||||
|
||||
# What's In This Document
|
||||
@@ -10,6 +16,8 @@ This website was created with [Docusaurus](https://docusaurus.io/).
|
||||
|
||||
# Getting Started
|
||||
|
||||
Testing the web site locally is a great way to see what final website will look like after publishing, and is ideal for testing more complex changes, large updates to navigation, or complex page designs that may include special alignment, which may not otherwise be validated through continuous integration.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
@@ -22,7 +30,7 @@ $ yarn install
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.
|
||||
This command starts a local development server and opens up a browser window. Most content changes made to the `docs/` root folder are reflected live without having to restart the server.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -57,9 +65,9 @@ my-docusaurus/
|
||||
siteConfig.js
|
||||
```
|
||||
|
||||
# Editing Content
|
||||
## Editing Content
|
||||
|
||||
## Editing an existing docs page
|
||||
### Editing an existing docs page
|
||||
|
||||
Edit docs by navigating to `docs/` and editing the corresponding document:
|
||||
|
||||
@@ -76,7 +84,7 @@ Edit me...
|
||||
|
||||
For more information about docs, click [here](https://docusaurus.io/docs/en/navigation)
|
||||
|
||||
## Editing an existing blog post
|
||||
### Editing an existing blog post
|
||||
|
||||
Edit blog posts by navigating to `website/blog` and editing the corresponding post:
|
||||
|
||||
@@ -93,9 +101,9 @@ Edit me...
|
||||
|
||||
For more information about blog posts, click [here](https://docusaurus.io/docs/en/adding-blog)
|
||||
|
||||
# Adding Content
|
||||
## Adding Content
|
||||
|
||||
## Adding a new docs page to an existing sidebar
|
||||
### Adding a new docs page to an existing sidebar
|
||||
|
||||
1. Create the doc as a new markdown file in `/docs`, example `docs/newly-created-doc.md`:
|
||||
|
||||
@@ -126,7 +134,7 @@ My new content here..
|
||||
|
||||
For more information about adding new docs, click [here](https://docusaurus.io/docs/en/navigation)
|
||||
|
||||
## Adding a new blog post
|
||||
### Adding a new blog post
|
||||
|
||||
1. Make sure there is a header link to your blog in `website/siteConfig.js`:
|
||||
|
||||
@@ -157,7 +165,7 @@ Lorem Ipsum...
|
||||
|
||||
For more information about blog posts, click [here](https://docusaurus.io/docs/en/adding-blog)
|
||||
|
||||
## Adding items to your site's top navigation bar
|
||||
### Adding items to your site's top navigation bar
|
||||
|
||||
1. Add links to docs, custom pages or external links by editing the headerLinks field of `website/siteConfig.js`:
|
||||
|
||||
@@ -181,7 +189,7 @@ For more information about blog posts, click [here](https://docusaurus.io/docs/e
|
||||
|
||||
For more information about the navigation bar, click [here](https://docusaurus.io/docs/en/navigation)
|
||||
|
||||
## Adding custom pages
|
||||
### Adding custom pages
|
||||
|
||||
1. Docusaurus uses React components to build pages. The components are saved as .js files in `website/pages/en`:
|
||||
1. If you want your page to show up in your navigation header, you will need to update `website/siteConfig.js` to add to the `headerLinks` element:
|
||||
@@ -199,11 +207,11 @@ For more information about the navigation bar, click [here](https://docusaurus.i
|
||||
}
|
||||
```
|
||||
|
||||
For more information about custom pages, click [here](https://docusaurus.io/docs/en/custom-pages).
|
||||
Learn more about [Docusaurus custom pages](https://docusaurus.io/docs/en/custom-pages).
|
||||
|
||||
# Full Documentation
|
||||
## Full Documentation
|
||||
|
||||
Full documentation can be found on the [website](https://docusaurus.io/).
|
||||
Full documentation can be found on the [Docusaurus website](https://docusaurus.io/).
|
||||
|
||||
## Additional notes
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ The only thing you need to do is to start the app:
|
||||
|
||||
```bash
|
||||
cd my-app
|
||||
yarn start
|
||||
yarn dev
|
||||
```
|
||||
|
||||
And you are good to go! 👍
|
||||
@@ -101,18 +101,12 @@ We provide a collection of public Backstage plugins (look for packages with the
|
||||
Install in your app’s package folder (`<root>/packages/app`) with:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-<plugin-name>
|
||||
```
|
||||
|
||||
Then add it to your app's `plugin.ts` file to import and register it:
|
||||
|
||||
`<root>/packages/app/src/plugin.ts`:
|
||||
|
||||
```js
|
||||
export { plugin as PluginName } from '@backstage/plugin-<plugin-name>';
|
||||
```
|
||||
|
||||
A plugin registers its own `route` in the app — read the documentation for the specific plugin you are installing for more information on that.
|
||||
After that, you inject the plugin into the application where you want it to be exposed. Please read the documentation for the specific plugin you are installing for more information.
|
||||
|
||||
### Creating an internal plugin
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@backstage/theme": "^0.2.6",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"@roadiehq/backstage-plugin-buildkite": "^1.0.0",
|
||||
"@roadiehq/backstage-plugin-github-insights": "^1.0.0",
|
||||
"@roadiehq/backstage-plugin-github-pull-requests": "^1.0.0",
|
||||
|
||||
@@ -8,6 +8,8 @@ error handling and similar.
|
||||
Add the library to your backend package:
|
||||
|
||||
```sh
|
||||
# From your Backstage root directory
|
||||
cd packages/backend
|
||||
yarn add @backstage/backend-common
|
||||
```
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import cors from 'cors';
|
||||
import Docker from 'dockerode';
|
||||
import { ErrorRequestHandler } from 'express';
|
||||
import express from 'express';
|
||||
import { Format } from 'logform';
|
||||
import { GithubCredentialsProvider } from '@backstage/integration';
|
||||
import { GitHubIntegration } from '@backstage/integration';
|
||||
import { GitLabIntegration } from '@backstage/integration';
|
||||
@@ -64,7 +63,7 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const coloredFormat: Format;
|
||||
export const coloredFormat: winston.Logform.Format;
|
||||
|
||||
// @public @deprecated
|
||||
export const createDatabase: typeof createDatabaseClient;
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/integration": "^0.5.1",
|
||||
"@google-cloud/storage": "^5.8.0",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"@types/cors": "^2.8.6",
|
||||
"@types/dockerode": "^3.2.1",
|
||||
"@types/express": "^4.17.6",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"@backstage/plugin-techdocs-backend": "^0.7.0",
|
||||
"@backstage/plugin-todo-backend": "^0.1.3",
|
||||
"@gitbeaker/node": "^28.0.2",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"azure-devops-node-api": "^10.1.1",
|
||||
"dockerode": "^3.2.1",
|
||||
"example-app": "^0.2.25",
|
||||
|
||||
@@ -42,7 +42,7 @@ export default async function createPlugin(
|
||||
} = await builder.build();
|
||||
|
||||
// TODO(jhaals): run and manage in background.
|
||||
processingEngine.start();
|
||||
await processingEngine.start();
|
||||
|
||||
return await createRouter({
|
||||
entitiesCatalog,
|
||||
|
||||
@@ -15,12 +15,11 @@
|
||||
*/
|
||||
|
||||
import { createRouter } from '@backstage/plugin-kubernetes-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config });
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ This package provides a CLI for developing Backstage plugins and apps.
|
||||
Install the package via npm or Yarn:
|
||||
|
||||
```sh
|
||||
$ npm install --save @backstage/cli
|
||||
npm install --save @backstage/cli
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
$ yarn add @backstage/cli
|
||||
yarn add @backstage/cli
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"lodash": "^4.17.19",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"ora": "^5.3.0",
|
||||
"postcss": "^8.1.0",
|
||||
"raw-loader": "^4.0.1",
|
||||
"react": "^16.0.0",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
|
||||
@@ -30,6 +30,7 @@ export default async (cmd: Command) => {
|
||||
...(await loadCliConfig({
|
||||
args: cmd.config,
|
||||
fromPackage: name,
|
||||
mockEnv: cmd.lax,
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ export function registerCommands(program: CommanderStatic) {
|
||||
.command('app:build')
|
||||
.description('Build an app for a production release')
|
||||
.option('--stats', 'Write bundle stats to output directory')
|
||||
.option('--lax', 'Do not require environment variables to be set')
|
||||
.option(...configOption)
|
||||
.action(lazy(() => import('./app/build').then(m => m.default)));
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ This package provides the core API used by Backstage plugins and apps.
|
||||
Do not install this package directly, it is reexported by `@backstage/core`. Install that instead:
|
||||
|
||||
```sh
|
||||
$ npm install --save @backstage/core
|
||||
npm install --save @backstage/core
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
$ yarn add @backstage/core
|
||||
yarn add @backstage/core
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -7,13 +7,13 @@ This package provides the core API used by Backstage plugins and apps.
|
||||
Install the package via npm or Yarn:
|
||||
|
||||
```sh
|
||||
$ npm install --save @backstage/core
|
||||
npm install --save @backstage/core
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
$ yarn add @backstage/core
|
||||
yarn add @backstage/core
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"d3-shape": "^2.0.0",
|
||||
"d3-zoom": "^2.0.0",
|
||||
"dagre": "^0.8.5",
|
||||
"history": "^5.0.0",
|
||||
"immer": "^9.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"material-table": "^1.69.1",
|
||||
@@ -57,7 +58,7 @@
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-hook-form": "^6.6.0",
|
||||
"react-hook-form": "^6.15.4",
|
||||
"react-markdown": "^5.0.2",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}",
|
||||
"@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}",
|
||||
"@gitbeaker/node": "^28.0.2",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"dockerode": "^3.2.1",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
|
||||
@@ -9,13 +9,13 @@ This package provides utilities that help in developing plugins for Backstage, l
|
||||
Install the package via npm or Yarn:
|
||||
|
||||
```sh
|
||||
$ npm install --save-dev @backstage/dev-utils
|
||||
npm install --save-dev @backstage/dev-utils
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
$ yarn add -D @backstage/dev-utils
|
||||
yarn add -D @backstage/dev-utils
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"@backstage/config": "^0.1.2",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"@octokit/auth-app": "^2.10.5",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"@octokit/auth-app": "^3.4.0",
|
||||
"luxon": "^1.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/theme": "^0.2.0"
|
||||
"@backstage/theme": "^0.2.0",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^6.1.11",
|
||||
|
||||
@@ -7,13 +7,13 @@ This package provides utilities that can be used to test plugins and apps for Ba
|
||||
Install the package via npm or Yarn:
|
||||
|
||||
```sh
|
||||
$ npm install --save-dev @backstage/test-utils
|
||||
npm install --save-dev @backstage/test-utils
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
$ yarn add -D @backstage/test-utils
|
||||
yarn add -D @backstage/test-utils
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -7,13 +7,13 @@ This package provides the extended Material UI Theme(s) that power Backstage.
|
||||
Install the package via npm or Yarn:
|
||||
|
||||
```sh
|
||||
$ npm install --save @backstage/theme
|
||||
npm install --save @backstage/theme
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
$ yarn add @backstage/theme
|
||||
yarn add @backstage/theme
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -28,15 +28,15 @@ To link that a component provides or consumes an API, see the [`providesApis`](h
|
||||
1. Install the API docs plugin
|
||||
|
||||
```bash
|
||||
# packages/app
|
||||
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-api-docs
|
||||
```
|
||||
|
||||
2. Add the `ApiExplorerPage` extension to the app:
|
||||
|
||||
```tsx
|
||||
// packages/app/src/App.tsx
|
||||
// In packages/app/src/App.tsx
|
||||
|
||||
import { ApiExplorerPage } from '@backstage/plugin-api-docs';
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
} from '@backstage/plugin-api-docs';
|
||||
|
||||
const apiPage = (
|
||||
<EntityLayoutWrapper>
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={6}>
|
||||
@@ -80,7 +80,7 @@ const apiPage = (
|
||||
</Grid>
|
||||
</Grid>
|
||||
</EntityLayout.Route>
|
||||
</EntityLayoutWrapper>
|
||||
</EntityLayout>
|
||||
);
|
||||
|
||||
// ...
|
||||
|
||||
@@ -96,14 +96,18 @@ describe('<ConsumedApisCard />', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'API',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
catalogApi.getEntities.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'API',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
|
||||
@@ -96,14 +96,18 @@ describe('<HasApisCard />', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'API',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
catalogApi.getEntities.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'API',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
|
||||
@@ -96,14 +96,18 @@ describe('<ProvidedApisCard />', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'API',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
catalogApi.getEntities.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'API',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
|
||||
@@ -101,14 +101,18 @@ describe('<ConsumingComponentsCard />', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
catalogApi.getEntities.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
|
||||
@@ -101,14 +101,18 @@ describe('<ProvidingComponentsCard />', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
catalogApi.getEntities.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
|
||||
@@ -7,7 +7,9 @@ This backend plugin can be installed to serve static content of a Backstage app.
|
||||
Add both this package and your local frontend app package as dependencies to your backend, for example
|
||||
|
||||
```bash
|
||||
yarn add @backstage/plugin-app-backend example-app
|
||||
# From your Backstage root directory
|
||||
cd packages/backend
|
||||
yarn add @backstage/plugin-app-backend app
|
||||
```
|
||||
|
||||
By adding the app package as a dependency we ensure that it is built as part of the backend, and that it can be resolved at runtime.
|
||||
|
||||
@@ -8,9 +8,9 @@ Welcome to the Bitrise plugin!
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
# The plugin must be added in the app package
|
||||
$ cd packages/app
|
||||
$ yarn add @backstage/plugin-bitrise
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-bitrise
|
||||
```
|
||||
|
||||
Bitrise Plugin exposes an entity tab component named `EntityBitriseContent`. You can include it in the
|
||||
@@ -22,7 +22,7 @@ import { EntityBitriseContent } from '@backstage/plugin-bitrise';
|
||||
|
||||
// Farther down at the website declaration
|
||||
const websiteEntityPage = (
|
||||
<EntityLayoutWrapper>
|
||||
<EntityLayout>
|
||||
{/* Place the following section where you want the tab to appear */}
|
||||
<EntityLayout.Route path="/bitrise" title="Bitrise">
|
||||
<EntityBitriseContent />
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
"@types/yup": "^0.29.8",
|
||||
"msw": "^0.21.2",
|
||||
"sqlite3": "^5.0.0",
|
||||
"supertest": "^6.1.3"
|
||||
"supertest": "^6.1.3",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
+88
-123
@@ -17,21 +17,73 @@ import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import {
|
||||
BitbucketClient,
|
||||
BitbucketRepositoryParser,
|
||||
PagedResponse,
|
||||
} from './bitbucket';
|
||||
import { BitbucketRepositoryParser, PagedResponse } from './bitbucket';
|
||||
import { results } from './index';
|
||||
import { RequestHandler, rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
function pagedResponse(values: any): PagedResponse<any> {
|
||||
return {
|
||||
values: values,
|
||||
isLastPage: true,
|
||||
} as PagedResponse<any>;
|
||||
const server = setupServer();
|
||||
|
||||
function setupStubs(projects: any[]) {
|
||||
function pagedResponse(values: any): PagedResponse<any> {
|
||||
return {
|
||||
values: values,
|
||||
isLastPage: true,
|
||||
} as PagedResponse<any>;
|
||||
}
|
||||
|
||||
function stubbedProject(
|
||||
project: string,
|
||||
repos: string[],
|
||||
): RequestHandler<any, any> {
|
||||
return rest.get(
|
||||
`https://bitbucket.mycompany.com/api/rest/1.0/projects/${project}/repos`,
|
||||
(_, res, ctx) => {
|
||||
const response = [];
|
||||
for (const repo of repos) {
|
||||
response.push({
|
||||
slug: repo,
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href: `https://bitbucket.mycompany.com/projects/${project}/repos/${repo}/browse`,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
return res(ctx.json(pagedResponse(response)));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
`https://bitbucket.mycompany.com/api/rest/1.0/projects`,
|
||||
(_, res, ctx) => {
|
||||
return res(
|
||||
ctx.json(
|
||||
pagedResponse(
|
||||
projects.map(p => {
|
||||
return { key: p.key };
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
for (const project of projects) {
|
||||
server.use(stubbedProject(project.key, project.repos));
|
||||
}
|
||||
}
|
||||
|
||||
describe('BitbucketDiscoveryProcessor', () => {
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('reject unrelated entries', () => {
|
||||
@@ -81,58 +133,29 @@ describe('BitbucketDiscoveryProcessor', () => {
|
||||
const processor = BitbucketDiscoveryProcessor.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [{ host: 'bitbucket.mycompany.com', token: 'blob' }],
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.mycompany.com',
|
||||
token: 'blob',
|
||||
apiBaseUrl: 'https://bitbucket.mycompany.com/api/rest/1.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{ logger: getVoidLogger() },
|
||||
);
|
||||
|
||||
it('output all repositories', async () => {
|
||||
setupStubs([
|
||||
{ key: 'backstage', repos: ['backstage'] },
|
||||
{ key: 'demo', repos: ['demo'] },
|
||||
]);
|
||||
const location: LocationSpec = {
|
||||
type: 'bitbucket-discovery',
|
||||
target:
|
||||
'https://bitbucket.mycompany.com/projects/*/repos/*/catalog.yaml',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(BitbucketClient.prototype, 'listProjects')
|
||||
.mockResolvedValue(
|
||||
pagedResponse([{ key: 'backstage' }, { key: 'demo' }]),
|
||||
);
|
||||
jest
|
||||
.spyOn(BitbucketClient.prototype, 'listRepositories')
|
||||
.mockResolvedValueOnce(
|
||||
pagedResponse([
|
||||
{
|
||||
slug: 'backstage',
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href:
|
||||
'https://bitbucket.mycompany.com/projects/backstage/repos/backstage/browse',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
jest
|
||||
.spyOn(BitbucketClient.prototype, 'listRepositories')
|
||||
.mockResolvedValueOnce(
|
||||
pagedResponse([
|
||||
{
|
||||
slug: 'demo',
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href:
|
||||
'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
const emitter = jest.fn();
|
||||
|
||||
await processor.readLocation(location, false, emitter);
|
||||
@@ -158,44 +181,16 @@ describe('BitbucketDiscoveryProcessor', () => {
|
||||
});
|
||||
|
||||
it('output repositories with wildcards', async () => {
|
||||
setupStubs([
|
||||
{ key: 'backstage', repos: ['backstage', 'techdocs-cli'] },
|
||||
{ key: 'demo', repos: ['demo'] },
|
||||
]);
|
||||
const location: LocationSpec = {
|
||||
type: 'bitbucket-discovery',
|
||||
target:
|
||||
'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-*/catalog.yaml',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(BitbucketClient.prototype, 'listProjects')
|
||||
.mockResolvedValue(pagedResponse([{ key: 'backstage' }]));
|
||||
jest
|
||||
.spyOn(BitbucketClient.prototype, 'listRepositories')
|
||||
.mockResolvedValueOnce(
|
||||
pagedResponse([
|
||||
{ slug: 'backstage' },
|
||||
{
|
||||
slug: 'techdocs-cli',
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href:
|
||||
'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'techdocs-container',
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href:
|
||||
'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-container/browse',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
const emitter = jest.fn();
|
||||
await processor.readLocation(location, false, emitter);
|
||||
|
||||
@@ -208,46 +203,15 @@ describe('BitbucketDiscoveryProcessor', () => {
|
||||
},
|
||||
optional: true,
|
||||
});
|
||||
expect(emitter).toHaveBeenCalledWith({
|
||||
type: 'location',
|
||||
location: {
|
||||
type: 'url',
|
||||
target:
|
||||
'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-container/browse/catalog.yaml',
|
||||
},
|
||||
optional: true,
|
||||
});
|
||||
});
|
||||
it('filter unrelated repositories', async () => {
|
||||
setupStubs([{ key: 'backstage', repos: ['test', 'abctest', 'testxyz'] }]);
|
||||
const location: LocationSpec = {
|
||||
type: 'bitbucket-discovery',
|
||||
target:
|
||||
'https://bitbucket.mycompany.com/projects/backstage/repos/test/catalog.yaml',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(BitbucketClient.prototype, 'listProjects')
|
||||
.mockResolvedValue(pagedResponse([{ key: 'backstage' }]));
|
||||
jest
|
||||
.spyOn(BitbucketClient.prototype, 'listRepositories')
|
||||
.mockResolvedValue(
|
||||
pagedResponse([
|
||||
{ slug: 'abstest' },
|
||||
{ slug: 'testxyz' },
|
||||
{
|
||||
slug: 'test',
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href:
|
||||
'https://bitbucket.mycompany.com/projects/backstage/repos/test',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const emitter = jest.fn();
|
||||
await processor.readLocation(location, false, emitter);
|
||||
|
||||
@@ -256,7 +220,7 @@ describe('BitbucketDiscoveryProcessor', () => {
|
||||
location: {
|
||||
type: 'url',
|
||||
target:
|
||||
'https://bitbucket.mycompany.com/projects/backstage/repos/test/catalog.yaml',
|
||||
'https://bitbucket.mycompany.com/projects/backstage/repos/test/browse/catalog.yaml',
|
||||
},
|
||||
optional: true,
|
||||
});
|
||||
@@ -277,26 +241,27 @@ describe('BitbucketDiscoveryProcessor', () => {
|
||||
const processor = BitbucketDiscoveryProcessor.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [{ host: 'bitbucket.mycompany.com', token: 'blob' }],
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.mycompany.com',
|
||||
token: 'blob',
|
||||
apiBaseUrl: 'https://bitbucket.mycompany.com/api/rest/1.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{ parser: customRepositoryParser, logger: getVoidLogger() },
|
||||
);
|
||||
|
||||
it('use custom repository parser', async () => {
|
||||
setupStubs([{ key: 'backstage', repos: ['test'] }]);
|
||||
|
||||
const location: LocationSpec = {
|
||||
type: 'bitbucket-discovery',
|
||||
target:
|
||||
'https://bitbucket.mycompany.com/projects/backstage/repos/test/catalog.yaml',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(BitbucketClient.prototype, 'listProjects')
|
||||
.mockResolvedValue(pagedResponse([{ key: 'backstage' }]));
|
||||
jest
|
||||
.spyOn(BitbucketClient.prototype, 'listRepositories')
|
||||
.mockResolvedValue(pagedResponse([{ slug: 'test' }]));
|
||||
|
||||
const emitter = jest.fn();
|
||||
await processor.readLocation(location, false, emitter);
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ import {
|
||||
} from '@backstage/integration';
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import {
|
||||
Repository,
|
||||
BitbucketRepositoryParser,
|
||||
BitbucketClient,
|
||||
defaultRepositoryParser,
|
||||
paginated,
|
||||
BitbucketRepository,
|
||||
} from './bitbucket';
|
||||
import { CatalogProcessor, CatalogProcessorEmit } from './types';
|
||||
|
||||
@@ -66,20 +66,19 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bitbucketConfig = this.integrations.bitbucket.byUrl(location.target)
|
||||
?.config;
|
||||
if (!bitbucketConfig) {
|
||||
const integration = this.integrations.bitbucket.byUrl(location.target);
|
||||
if (!integration) {
|
||||
throw new Error(
|
||||
`There is no Bitbucket integration that matches ${location.target}. Please add a configuration entry for it under integrations.bitbucket`,
|
||||
);
|
||||
} else if (bitbucketConfig.host === 'bitbucket.org') {
|
||||
} else if (integration.config.host === 'bitbucket.org') {
|
||||
throw new Error(
|
||||
`Component discovery for Bitbucket Cloud is not yet supported`,
|
||||
);
|
||||
}
|
||||
|
||||
const client = new BitbucketClient({
|
||||
config: bitbucketConfig,
|
||||
config: integration.config,
|
||||
});
|
||||
const startTimestamp = Date.now();
|
||||
this.logger.info(`Reading Bitbucket repositories from ${location.target}`);
|
||||
@@ -90,9 +89,9 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor {
|
||||
|
||||
for (const repository of result.matches) {
|
||||
for await (const entity of this.parser({
|
||||
client: client,
|
||||
repository: repository,
|
||||
path: catalogPath,
|
||||
integration: integration,
|
||||
target: `${repository.links.self[0].href}${catalogPath}`,
|
||||
logger: this.logger,
|
||||
})) {
|
||||
emit(entity);
|
||||
}
|
||||
@@ -159,5 +158,5 @@ function escapeRegExp(str: string): RegExp {
|
||||
|
||||
type Result = {
|
||||
scanned: number;
|
||||
matches: Repository[];
|
||||
matches: BitbucketRepository[];
|
||||
};
|
||||
|
||||
+5
-11
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { defaultRepositoryParser } from './BitbucketRepositoryParser';
|
||||
import { Project, Repository } from './types';
|
||||
import { BitbucketClient } from './client';
|
||||
import { results } from '../index';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { BitbucketIntegration } from '@backstage/integration';
|
||||
|
||||
describe('BitbucketRepositoryParser', () => {
|
||||
describe('defaultRepositoryParser', () => {
|
||||
@@ -34,15 +34,9 @@ describe('BitbucketRepositoryParser', () => {
|
||||
),
|
||||
];
|
||||
const actual = await defaultRepositoryParser({
|
||||
client: {} as BitbucketClient,
|
||||
repository: {
|
||||
project: {} as Project,
|
||||
slug: 'repo-slug',
|
||||
links: {
|
||||
self: [{ href: browseUrl }],
|
||||
},
|
||||
} as Repository,
|
||||
path: path,
|
||||
integration: {} as BitbucketIntegration,
|
||||
target: `${browseUrl}${path}`,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
let i = 0;
|
||||
|
||||
+7
-8
@@ -13,25 +13,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Repository } from './types';
|
||||
import { CatalogProcessorResult } from '../types';
|
||||
import { results } from '../index';
|
||||
import { BitbucketClient } from './client';
|
||||
import { Logger } from 'winston';
|
||||
import { BitbucketIntegration } from '@backstage/integration';
|
||||
|
||||
export type BitbucketRepositoryParser = (options: {
|
||||
client: BitbucketClient;
|
||||
repository: Repository;
|
||||
path: string;
|
||||
integration: BitbucketIntegration;
|
||||
target: string;
|
||||
logger: Logger;
|
||||
}) => AsyncIterable<CatalogProcessorResult>;
|
||||
|
||||
export const defaultRepositoryParser: BitbucketRepositoryParser = async function* defaultRepositoryParser({
|
||||
repository,
|
||||
path,
|
||||
target,
|
||||
}) {
|
||||
yield results.location(
|
||||
{
|
||||
type: 'url',
|
||||
target: `${repository.links.self[0].href}${path}`,
|
||||
target: target,
|
||||
},
|
||||
// Not all locations may actually exist, since the user defined them as a wildcard pattern.
|
||||
// Thus, we emit them as optional and let the downstream processor find them while not outputting
|
||||
|
||||
@@ -41,15 +41,6 @@ export class BitbucketClient {
|
||||
);
|
||||
}
|
||||
|
||||
async getRaw(
|
||||
projectKey: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
): Promise<Response> {
|
||||
const request = `${this.config.apiBaseUrl}/projects/${projectKey}/repos/${repo}/raw/${path}`;
|
||||
return fetch(request, getBitbucketRequestOptions(this.config));
|
||||
}
|
||||
|
||||
private async pagedRequest(
|
||||
endpoint: string,
|
||||
options?: ListOptions,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { BitbucketClient, paginated } from './client';
|
||||
export type { PagedResponse } from './client';
|
||||
export * from './types';
|
||||
export type { BitbucketRepositoryParser } from './BitbucketRepositoryParser';
|
||||
export { defaultRepositoryParser } from './BitbucketRepositoryParser';
|
||||
export type { PagedResponse } from './client';
|
||||
export type { BitbucketRepository } from './types';
|
||||
export type { BitbucketRepositoryParser } from './BitbucketRepositoryParser';
|
||||
|
||||
@@ -13,16 +13,15 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export type Project = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
export type Repository = {
|
||||
project: Project;
|
||||
export type BitbucketRepository = {
|
||||
project: {
|
||||
key: string;
|
||||
};
|
||||
slug: string;
|
||||
links: Record<string, Link[]>;
|
||||
};
|
||||
|
||||
export type Link = {
|
||||
href: string;
|
||||
links: Record<
|
||||
string,
|
||||
{
|
||||
href: string;
|
||||
}[]
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -156,7 +156,7 @@ describe('github', () => {
|
||||
describe('getOrganizationRepositories', () => {
|
||||
it('read repositories', async () => {
|
||||
const input: QueryResponse = {
|
||||
organization: {
|
||||
repositoryOwner: {
|
||||
repositories: {
|
||||
nodes: [
|
||||
{
|
||||
|
||||
@@ -20,7 +20,8 @@ import { graphql } from '@octokit/graphql';
|
||||
// Graphql types
|
||||
|
||||
export type QueryResponse = {
|
||||
organization: Organization;
|
||||
organization?: Organization;
|
||||
repositoryOwner?: Organization | User;
|
||||
};
|
||||
|
||||
export type Organization = {
|
||||
@@ -41,6 +42,7 @@ export type User = {
|
||||
avatarUrl?: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
repositories?: Connection<Repository>;
|
||||
};
|
||||
|
||||
export type Team = {
|
||||
@@ -228,28 +230,27 @@ export async function getOrganizationRepositories(
|
||||
org: string,
|
||||
): Promise<{ repositories: Repository[] }> {
|
||||
const query = `
|
||||
query repositories($org: String!, $cursor: String) {
|
||||
organization(login: $org) {
|
||||
name
|
||||
repositories(first: 100, after: $cursor) {
|
||||
nodes {
|
||||
name
|
||||
url
|
||||
isArchived
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
query repositories($org: String!, $cursor: String) {
|
||||
repositoryOwner(login: $org) {
|
||||
login
|
||||
repositories(first: 100, after: $cursor) {
|
||||
nodes {
|
||||
name
|
||||
url
|
||||
isArchived
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}`;
|
||||
|
||||
const repositories = await queryWithPaging(
|
||||
client,
|
||||
query,
|
||||
r => r.organization?.repositories,
|
||||
r => r.repositoryOwner?.repositories,
|
||||
x => x,
|
||||
{ org },
|
||||
);
|
||||
|
||||
@@ -35,3 +35,5 @@ export * from './types';
|
||||
export { UrlReaderProcessor } from './UrlReaderProcessor';
|
||||
export { parseEntityYaml } from './util/parse';
|
||||
export { results };
|
||||
|
||||
export type { BitbucketRepositoryParser } from './bitbucket';
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { TransactionValue } from './TransactionValue';
|
||||
import { Knex } from 'knex';
|
||||
import { BackgroundContext } from './BackgroundContext';
|
||||
|
||||
describe('TransactionValue Context', () => {
|
||||
it('should be able to store tx values and retrieve them from a context', () => {
|
||||
const tx = {} as Knex.Transaction;
|
||||
const ctx = new BackgroundContext();
|
||||
|
||||
const nextCtx = TransactionValue.in(ctx, tx);
|
||||
|
||||
expect(TransactionValue.from(nextCtx)).toBe(tx);
|
||||
});
|
||||
|
||||
it('should throw when there is no tx value in the context', () => {
|
||||
const ctx = new BackgroundContext();
|
||||
|
||||
expect(() => TransactionValue.from(ctx)).toThrow(
|
||||
/No transaction available in context/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
|
||||
|
||||
import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine';
|
||||
import { Stitcher } from './Stitcher';
|
||||
import { CatalogProcessingOrchestrator } from './types';
|
||||
import waitForExpect from 'wait-for-expect';
|
||||
|
||||
describe('DefaultCatalogProcessingEngine', () => {
|
||||
const db = ({
|
||||
transaction: jest.fn(),
|
||||
getProcessableEntities: jest.fn(),
|
||||
updateProcessedEntity: jest.fn(),
|
||||
} as unknown) as jest.Mocked<DefaultProcessingDatabase>;
|
||||
const orchestrator: jest.Mocked<CatalogProcessingOrchestrator> = {
|
||||
process: jest.fn(),
|
||||
};
|
||||
const stitcher = ({
|
||||
stitch: jest.fn(),
|
||||
} as unknown) as jest.Mocked<Stitcher>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('should process stuff', async () => {
|
||||
orchestrator.process.mockResolvedValue({
|
||||
ok: true,
|
||||
completedEntity: {
|
||||
apiVersion: '1',
|
||||
kind: 'Location',
|
||||
metadata: { name: 'test' },
|
||||
},
|
||||
relations: [],
|
||||
errors: [],
|
||||
deferredEntities: [],
|
||||
state: new Map(),
|
||||
});
|
||||
const engine = new DefaultCatalogProcessingEngine(
|
||||
getVoidLogger(),
|
||||
[],
|
||||
db,
|
||||
orchestrator,
|
||||
stitcher,
|
||||
);
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
|
||||
db.getProcessableEntities
|
||||
.mockImplementation(async () => {
|
||||
await engine.stop();
|
||||
return { items: [] };
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
items: [
|
||||
{
|
||||
entityRef: 'foo',
|
||||
id: '1',
|
||||
unprocessedEntity: {
|
||||
apiVersion: '1',
|
||||
kind: 'Location',
|
||||
metadata: { name: 'test' },
|
||||
},
|
||||
state: new Map(),
|
||||
nextUpdateAt: '',
|
||||
lastDiscoveryAt: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await engine.start();
|
||||
await waitForExpect(() => {
|
||||
expect(orchestrator.process).toBeCalledTimes(1);
|
||||
expect(orchestrator.process).toBeCalledWith({
|
||||
entity: {
|
||||
apiVersion: '1',
|
||||
kind: 'Location',
|
||||
metadata: { name: 'test' },
|
||||
},
|
||||
state: expect.anything(),
|
||||
});
|
||||
});
|
||||
await engine.stop();
|
||||
});
|
||||
|
||||
it('should process stuff even if the first attempt fail', async () => {
|
||||
orchestrator.process.mockResolvedValue({
|
||||
ok: true,
|
||||
completedEntity: {
|
||||
apiVersion: '1',
|
||||
kind: 'Location',
|
||||
metadata: { name: 'test' },
|
||||
},
|
||||
relations: [],
|
||||
errors: [],
|
||||
deferredEntities: [],
|
||||
state: new Map(),
|
||||
});
|
||||
const engine = new DefaultCatalogProcessingEngine(
|
||||
getVoidLogger(),
|
||||
[],
|
||||
db,
|
||||
orchestrator,
|
||||
stitcher,
|
||||
);
|
||||
|
||||
db.transaction.mockImplementation(cb => cb((() => {}) as any));
|
||||
|
||||
db.getProcessableEntities
|
||||
.mockImplementation(async () => {
|
||||
await engine.stop();
|
||||
return { items: [] };
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('I FAILED'))
|
||||
.mockResolvedValueOnce({
|
||||
items: [
|
||||
{
|
||||
entityRef: 'foo',
|
||||
id: '1',
|
||||
unprocessedEntity: {
|
||||
apiVersion: '1',
|
||||
kind: 'Location',
|
||||
metadata: { name: 'test' },
|
||||
},
|
||||
state: new Map(),
|
||||
nextUpdateAt: '',
|
||||
lastDiscoveryAt: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await engine.start();
|
||||
await waitForExpect(() => {
|
||||
expect(orchestrator.process).toBeCalledTimes(1);
|
||||
expect(orchestrator.process).toBeCalledWith({
|
||||
entity: {
|
||||
apiVersion: '1',
|
||||
kind: 'Location',
|
||||
metadata: { name: 'test' },
|
||||
},
|
||||
state: expect.anything(),
|
||||
});
|
||||
});
|
||||
await engine.stop();
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { Logger } from 'winston';
|
||||
import { ProcessingDatabase } from './database/types';
|
||||
import { Stitcher } from './Stitcher';
|
||||
import {
|
||||
CatalogProcessingEngine,
|
||||
@@ -23,44 +24,46 @@ import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
EntityProviderMutation,
|
||||
ProcessingStateManager,
|
||||
} from './types';
|
||||
|
||||
class Connection implements EntityProviderConnection {
|
||||
constructor(
|
||||
private readonly config: {
|
||||
stateManager: ProcessingStateManager;
|
||||
processingDatabase: ProcessingDatabase;
|
||||
id: string;
|
||||
},
|
||||
) {}
|
||||
|
||||
async applyMutation(mutation: EntityProviderMutation): Promise<void> {
|
||||
const db = this.config.processingDatabase;
|
||||
if (mutation.type === 'full') {
|
||||
await this.config.stateManager.replaceProcessingItems({
|
||||
sourceKey: this.config.id,
|
||||
type: 'full',
|
||||
items: mutation.entities,
|
||||
await db.transaction(async tx => {
|
||||
await db.replaceUnprocessedEntities(tx, {
|
||||
sourceKey: this.config.id,
|
||||
type: 'full',
|
||||
items: mutation.entities,
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.config.stateManager.replaceProcessingItems({
|
||||
sourceKey: this.config.id,
|
||||
type: 'delta',
|
||||
added: mutation.added,
|
||||
removed: mutation.removed,
|
||||
await db.transaction(async tx => {
|
||||
await db.replaceUnprocessedEntities(tx, {
|
||||
sourceKey: this.config.id,
|
||||
type: 'delta',
|
||||
added: mutation.added,
|
||||
removed: mutation.removed,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
|
||||
private running: boolean = false;
|
||||
private running = false;
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly entityProviders: EntityProvider[],
|
||||
private readonly stateManager: ProcessingStateManager,
|
||||
private readonly processingDatabase: ProcessingDatabase,
|
||||
private readonly orchestrator: CatalogProcessingOrchestrator,
|
||||
private readonly stitcher: Stitcher,
|
||||
) {}
|
||||
@@ -69,52 +72,77 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
|
||||
for (const provider of this.entityProviders) {
|
||||
await provider.connect(
|
||||
new Connection({
|
||||
stateManager: this.stateManager,
|
||||
processingDatabase: this.processingDatabase,
|
||||
id: provider.getProviderName(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.running = true;
|
||||
this.run();
|
||||
}
|
||||
|
||||
private async run() {
|
||||
while (this.running) {
|
||||
const {
|
||||
id,
|
||||
entity,
|
||||
state: initialState,
|
||||
} = await this.stateManager.getNextProcessingItem();
|
||||
try {
|
||||
// TODO: We want to disconnect the queue popping and message processing
|
||||
// so that if the queue popping fails we exponentially back off in order to give the DB room to sort itself out.
|
||||
await this.process();
|
||||
} catch (e) {
|
||||
this.logger.warn('Processing failed with:', e);
|
||||
// TODO: this can be a little smarter as mentioned in the above comment.
|
||||
// But for now, if something fails, wait a brief time to pick up the next message.
|
||||
await this.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.orchestrator.process({
|
||||
entity,
|
||||
state: initialState,
|
||||
private async process() {
|
||||
const { items } = await this.processingDatabase.transaction(async tx => {
|
||||
return this.processingDatabase.getProcessableEntities(tx, {
|
||||
processBatchSize: 1,
|
||||
});
|
||||
});
|
||||
|
||||
for (const error of result.errors) {
|
||||
this.logger.warn(error.message);
|
||||
}
|
||||
if (!items.length) {
|
||||
// No items to process, wait and try again.
|
||||
await this.wait();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
const { id, state, unprocessedEntity } = items[0];
|
||||
|
||||
result.completedEntity.metadata.uid = id;
|
||||
await this.stateManager.setProcessingItemResult({
|
||||
const result = await this.orchestrator.process({
|
||||
entity: unprocessedEntity,
|
||||
state,
|
||||
});
|
||||
for (const error of result.errors) {
|
||||
this.logger.warn(error.message);
|
||||
}
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.completedEntity.metadata.uid = id;
|
||||
await this.processingDatabase.transaction(async tx => {
|
||||
await this.processingDatabase.updateProcessedEntity(tx, {
|
||||
id,
|
||||
entity: result.completedEntity,
|
||||
processedEntity: result.completedEntity,
|
||||
state: result.state,
|
||||
errors: result.errors,
|
||||
errors: JSON.stringify(result.errors),
|
||||
relations: result.relations,
|
||||
deferredEntities: result.deferredEntities,
|
||||
});
|
||||
});
|
||||
|
||||
const setOfThingsToStitch = new Set<string>([
|
||||
stringifyEntityRef(result.completedEntity),
|
||||
...result.relations.map(relation =>
|
||||
stringifyEntityRef(relation.source),
|
||||
),
|
||||
]);
|
||||
await this.stitcher.stitch(setOfThingsToStitch);
|
||||
}
|
||||
const setOfThingsToStitch = new Set<string>([
|
||||
stringifyEntityRef(result.completedEntity),
|
||||
...result.relations.map(relation => stringifyEntityRef(relation.source)),
|
||||
]);
|
||||
await this.stitcher.stitch(setOfThingsToStitch);
|
||||
}
|
||||
|
||||
private async wait() {
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
async stop() {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ProcessingDatabase } from './database/types';
|
||||
import {
|
||||
ProcessingItem,
|
||||
ProcessingItemResult,
|
||||
ProcessingStateManager,
|
||||
ReplaceProcessingItemsRequest,
|
||||
} from './types';
|
||||
|
||||
export class DefaultProcessingStateManager implements ProcessingStateManager {
|
||||
constructor(private readonly db: ProcessingDatabase) {}
|
||||
|
||||
replaceProcessingItems(
|
||||
request: ReplaceProcessingItemsRequest,
|
||||
): Promise<void> {
|
||||
return this.db.transaction(async tx => {
|
||||
await this.db.replaceUnprocessedEntities(tx, request);
|
||||
});
|
||||
}
|
||||
|
||||
async setProcessingItemResult(result: ProcessingItemResult) {
|
||||
return this.db.transaction(async tx => {
|
||||
await this.db.updateProcessedEntity(tx, {
|
||||
id: result.id,
|
||||
processedEntity: result.entity,
|
||||
errors: JSON.stringify(result.errors),
|
||||
state: result.state,
|
||||
relations: result.relations,
|
||||
deferredEntities: result.deferredEntities,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getNextProcessingItem(): Promise<ProcessingItem> {
|
||||
for (;;) {
|
||||
const { items } = await this.db.transaction(async tx => {
|
||||
return this.db.getProcessableEntities(tx, {
|
||||
processBatchSize: 1,
|
||||
});
|
||||
});
|
||||
|
||||
if (items.length) {
|
||||
const { id, state, unprocessedEntity } = items[0];
|
||||
return {
|
||||
id,
|
||||
entity: unprocessedEntity,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,6 @@ import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase'
|
||||
import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine';
|
||||
import { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator';
|
||||
import { DefaultLocationStore } from './DefaultLocationStore';
|
||||
import { DefaultProcessingStateManager } from './DefaultProcessingStateManager';
|
||||
import { NextEntitiesCatalog } from './NextEntitiesCatalog';
|
||||
import { Stitcher } from './Stitcher';
|
||||
|
||||
@@ -252,7 +251,6 @@ export class NextCatalogBuilder {
|
||||
const db = new CommonDatabase(dbClient, logger);
|
||||
|
||||
const processingDatabase = new DefaultProcessingDatabase(dbClient, logger);
|
||||
const stateManager = new DefaultProcessingStateManager(processingDatabase);
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
const orchestrator = new DefaultCatalogProcessingOrchestrator({
|
||||
processors,
|
||||
@@ -269,7 +267,7 @@ export class NextCatalogBuilder {
|
||||
const processingEngine = new DefaultCatalogProcessingEngine(
|
||||
logger,
|
||||
[locationStore, configLocationProvider],
|
||||
stateManager,
|
||||
processingDatabase,
|
||||
orchestrator,
|
||||
stitcher,
|
||||
);
|
||||
|
||||
@@ -23,15 +23,26 @@ import {
|
||||
DefaultProcessingDatabase,
|
||||
} from './DefaultProcessingDatabase';
|
||||
|
||||
import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import * as uuid from 'uuid';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
|
||||
describe('Default Processing Database', () => {
|
||||
let db: Knex;
|
||||
let processingDatabase: DefaultProcessingDatabase;
|
||||
const logger = getVoidLogger();
|
||||
|
||||
const insertRefRow = async (ref: DbRefreshStateReferencesRow) => {
|
||||
return db<DbRefreshStateReferencesRow>('refresh_state_references').insert(
|
||||
ref,
|
||||
);
|
||||
};
|
||||
|
||||
const insertRefreshStateRow = async (ref: DbRefreshStateRow) => {
|
||||
await db<DbRefreshStateRow>('refresh_state').insert(ref);
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await DatabaseManager.createTestDatabaseConnection();
|
||||
await DatabaseManager.createDatabase(db);
|
||||
@@ -39,17 +50,162 @@ describe('Default Processing Database', () => {
|
||||
processingDatabase = new DefaultProcessingDatabase(db, logger);
|
||||
});
|
||||
|
||||
describe('replaceUnprocessedEntities', () => {
|
||||
const insertRefRow = async (ref: DbRefreshStateReferencesRow) => {
|
||||
return db<DbRefreshStateReferencesRow>('refresh_state_references').insert(
|
||||
ref,
|
||||
describe('updateProcessedEntity', () => {
|
||||
let id: string;
|
||||
let processedEntity: Entity;
|
||||
|
||||
beforeEach(() => {
|
||||
id = uuid.v4();
|
||||
processedEntity = {
|
||||
apiVersion: '1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
name: 'fakelocation',
|
||||
},
|
||||
spec: {
|
||||
type: 'url',
|
||||
target: 'somethingelse',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('fails when there is no processing state for the entity', async () => {
|
||||
await processingDatabase.transaction(async tx => {
|
||||
await expect(() =>
|
||||
processingDatabase.updateProcessedEntity(tx, {
|
||||
id,
|
||||
processedEntity,
|
||||
state: new Map<string, JsonObject>(),
|
||||
relations: [],
|
||||
deferredEntities: [],
|
||||
}),
|
||||
).rejects.toThrow(`Processing state not found for ${id}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the refresh state entry with the cache, processed entity and errors', async () => {
|
||||
await insertRefreshStateRow({
|
||||
entity_id: id,
|
||||
entity_ref: 'location:default/fakelocation',
|
||||
unprocessed_entity: '{}',
|
||||
processed_entity: '{}',
|
||||
errors: '[]',
|
||||
next_update_at: '2021-04-01 13:37:00',
|
||||
last_discovery_at: '2021-04-01 13:37:00',
|
||||
});
|
||||
|
||||
const state = new Map<string, JsonObject>();
|
||||
state.set('hello', { t: 'something' });
|
||||
|
||||
await processingDatabase.transaction(tx =>
|
||||
processingDatabase.updateProcessedEntity(tx, {
|
||||
id,
|
||||
processedEntity,
|
||||
state,
|
||||
relations: [],
|
||||
deferredEntities: [],
|
||||
errors: "['something broke']",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const insertRefreshStateRow = async (ref: DbRefreshStateRow) => {
|
||||
await db<DbRefreshStateRow>('refresh_state').insert(ref);
|
||||
};
|
||||
const entities = await db<DbRefreshStateRow>('refresh_state').select();
|
||||
expect(entities.length).toBe(1);
|
||||
expect(entities[0].processed_entity).toEqual(
|
||||
JSON.stringify(processedEntity),
|
||||
);
|
||||
expect(entities[0].cache).toEqual(JSON.stringify(state));
|
||||
expect(entities[0].errors).toEqual("['something broke']");
|
||||
});
|
||||
|
||||
it('removes old relations and stores the new relationships', async () => {
|
||||
await insertRefreshStateRow({
|
||||
entity_id: id,
|
||||
entity_ref: 'location:default/fakelocation',
|
||||
unprocessed_entity: '{}',
|
||||
processed_entity: '{}',
|
||||
errors: '[]',
|
||||
next_update_at: '2021-04-01 13:37:00',
|
||||
last_discovery_at: '2021-04-01 13:37:00',
|
||||
});
|
||||
|
||||
const relations = [
|
||||
{
|
||||
source: {
|
||||
kind: 'Component',
|
||||
namespace: 'Default',
|
||||
name: 'foo',
|
||||
},
|
||||
target: {
|
||||
kind: 'Component',
|
||||
namespace: 'Default',
|
||||
name: 'foo',
|
||||
},
|
||||
type: 'memberOf',
|
||||
},
|
||||
];
|
||||
|
||||
await processingDatabase.transaction(tx =>
|
||||
processingDatabase.updateProcessedEntity(tx, {
|
||||
id,
|
||||
processedEntity,
|
||||
state: new Map<string, JsonObject>(),
|
||||
relations: relations,
|
||||
deferredEntities: [],
|
||||
}),
|
||||
);
|
||||
|
||||
const savedRelations = await db<DbRelationsRow>('relations')
|
||||
.where({ originating_entity_id: id })
|
||||
.select();
|
||||
expect(savedRelations.length).toBe(1);
|
||||
expect(savedRelations[0]).toEqual({
|
||||
originating_entity_id: id,
|
||||
source_entity_ref: 'component:default/foo',
|
||||
type: 'memberOf',
|
||||
target_entity_ref: 'component:default/foo',
|
||||
});
|
||||
});
|
||||
|
||||
it('adds deferred entities to the the refresh_state table to be picked up later', async () => {
|
||||
await insertRefreshStateRow({
|
||||
entity_id: id,
|
||||
entity_ref: 'location:default/fakelocation',
|
||||
unprocessed_entity: '{}',
|
||||
processed_entity: '{}',
|
||||
errors: '[]',
|
||||
next_update_at: '2021-04-01 13:37:00',
|
||||
last_discovery_at: '2021-04-01 13:37:00',
|
||||
});
|
||||
|
||||
const deferredEntities = [
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
name: 'next',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await processingDatabase.transaction(tx =>
|
||||
processingDatabase.updateProcessedEntity(tx, {
|
||||
id,
|
||||
processedEntity,
|
||||
state: new Map<string, JsonObject>(),
|
||||
relations: [],
|
||||
deferredEntities,
|
||||
}),
|
||||
);
|
||||
|
||||
const refreshStateEntries = await db<DbRefreshStateRow>('refresh_state')
|
||||
.where({ entity_ref: stringifyEntityRef(deferredEntities[0]) })
|
||||
.select();
|
||||
|
||||
expect(refreshStateEntries).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceUnprocessedEntities', () => {
|
||||
const createLocations = async (entityRefs: string[]) => {
|
||||
for (const ref of entityRefs) {
|
||||
await insertRefreshStateRow({
|
||||
@@ -57,9 +213,9 @@ describe('Default Processing Database', () => {
|
||||
entity_ref: ref,
|
||||
unprocessed_entity: '{}',
|
||||
processed_entity: '{}',
|
||||
errors: '',
|
||||
next_update_at: 'now()',
|
||||
last_discovery_at: 'now()',
|
||||
errors: '[]',
|
||||
next_update_at: '2021-04-01 13:37:00',
|
||||
last_discovery_at: '2021-04-01 13:37:00',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -101,8 +257,8 @@ describe('Default Processing Database', () => {
|
||||
target_entity_ref: 'location:default/root-2',
|
||||
});
|
||||
|
||||
await processingDatabase.transaction(async tx => {
|
||||
await processingDatabase.replaceUnprocessedEntities(tx, {
|
||||
await processingDatabase.transaction(tx =>
|
||||
processingDatabase.replaceUnprocessedEntities(tx, {
|
||||
type: 'full',
|
||||
sourceKey: 'config',
|
||||
items: [
|
||||
@@ -114,8 +270,8 @@ describe('Default Processing Database', () => {
|
||||
kind: 'Location',
|
||||
} as Entity,
|
||||
],
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const currentRefreshState = await db<DbRefreshStateRow>(
|
||||
'refresh_state',
|
||||
@@ -431,86 +587,48 @@ describe('Default Processing Database', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateProcessedEntity', () => {
|
||||
it('should throw if the entity does not exist', async () => {
|
||||
await processingDatabase.transaction(async tx => {
|
||||
await expect(
|
||||
processingDatabase.updateProcessedEntity(tx, {
|
||||
id: '9',
|
||||
processedEntity: {
|
||||
apiVersion: '1.0.0',
|
||||
metadata: {
|
||||
name: 'new-root',
|
||||
},
|
||||
kind: 'Location',
|
||||
} as Entity,
|
||||
deferredEntities: [],
|
||||
relations: [],
|
||||
}),
|
||||
).rejects.toThrow('Processing state not found for 9');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update a processed entity', async () => {
|
||||
await db<DbRefreshStateRow>('refresh_state').insert({
|
||||
entity_id: '321',
|
||||
entity_ref: 'location:default/new-root',
|
||||
unprocessed_entity: '',
|
||||
errors: '',
|
||||
next_update_at: 'now()',
|
||||
last_discovery_at: 'now()',
|
||||
});
|
||||
|
||||
const deferredEntity = {
|
||||
describe('getProcessableEntities', () => {
|
||||
it('should return entities to process', async () => {
|
||||
const entity = JSON.stringify({
|
||||
kind: 'Location',
|
||||
apiVersion: '1.0.0',
|
||||
metadata: {
|
||||
name: 'deferred',
|
||||
name: 'xyz',
|
||||
},
|
||||
kind: 'Location',
|
||||
} as Entity;
|
||||
} as Entity);
|
||||
|
||||
const relation: EntityRelationSpec = {
|
||||
source: {
|
||||
kind: 'Component',
|
||||
namespace: 'Default',
|
||||
name: 'foo',
|
||||
},
|
||||
target: {
|
||||
kind: 'Component',
|
||||
namespace: 'Default',
|
||||
name: 'foo',
|
||||
},
|
||||
type: 'url',
|
||||
};
|
||||
|
||||
await processingDatabase.transaction(async tx => {
|
||||
await processingDatabase.updateProcessedEntity(tx, {
|
||||
id: '321',
|
||||
processedEntity: {
|
||||
apiVersion: '1.0.0',
|
||||
metadata: {
|
||||
name: 'new-root',
|
||||
},
|
||||
kind: 'Location',
|
||||
} as Entity,
|
||||
deferredEntities: [deferredEntity],
|
||||
relations: [relation],
|
||||
});
|
||||
await db<DbRefreshStateRow>('refresh_state').insert({
|
||||
entity_id: '2',
|
||||
entity_ref: 'location:default/new-root',
|
||||
unprocessed_entity: entity,
|
||||
errors: '[]',
|
||||
next_update_at: '2019-01-01 23:00:00',
|
||||
last_discovery_at: '2021-04-01 13:37:00',
|
||||
});
|
||||
|
||||
const deferredResult = await db<DbRefreshStateRow>('refresh_state')
|
||||
.where({ entity_ref: 'location:default/deferred' })
|
||||
.select();
|
||||
expect(deferredResult.length).toBe(1);
|
||||
await db<DbRefreshStateRow>('refresh_state').insert({
|
||||
entity_id: '1',
|
||||
entity_ref: 'location:default/foobar',
|
||||
unprocessed_entity: entity,
|
||||
errors: '[]',
|
||||
next_update_at: '2042-01-01 23:00:00',
|
||||
last_discovery_at: '2021-04-01 13:37:00',
|
||||
});
|
||||
|
||||
const [relations] = await db<DbRelationsRow>('relations')
|
||||
.where({ originating_entity_id: '321' })
|
||||
.select();
|
||||
expect(relations).toEqual({
|
||||
originating_entity_id: '321',
|
||||
source_entity_ref: 'component:default/foo',
|
||||
type: 'url',
|
||||
target_entity_ref: 'component:default/foo',
|
||||
await processingDatabase.transaction(async tx => {
|
||||
// request two items but only one can be processed.
|
||||
const result = await processingDatabase.getProcessableEntities(tx, {
|
||||
processBatchSize: 2,
|
||||
});
|
||||
expect(result.items.length).toEqual(1);
|
||||
expect(result.items[0].entityRef).toEqual('location:default/new-root');
|
||||
|
||||
// should not return the same item as there's nothing left to process.
|
||||
await expect(
|
||||
processingDatabase.getProcessableEntities(tx, {
|
||||
processBatchSize: 2,
|
||||
}),
|
||||
).resolves.toEqual({ items: [] });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,37 +80,3 @@ export type EntityProcessingResult =
|
||||
export interface CatalogProcessingOrchestrator {
|
||||
process(request: EntityProcessingRequest): Promise<EntityProcessingResult>;
|
||||
}
|
||||
|
||||
export type ProcessingItemResult = {
|
||||
id: string;
|
||||
entity: Entity;
|
||||
state: Map<string, JsonObject>;
|
||||
errors: Error[];
|
||||
relations: EntityRelationSpec[];
|
||||
deferredEntities: Entity[];
|
||||
};
|
||||
|
||||
export type ProcessingItem = {
|
||||
id: string;
|
||||
entity: Entity;
|
||||
state: Map<string, JsonObject>;
|
||||
};
|
||||
|
||||
export type ReplaceProcessingItemsRequest =
|
||||
| {
|
||||
sourceKey: string;
|
||||
items: Entity[];
|
||||
type: 'full';
|
||||
}
|
||||
| {
|
||||
sourceKey: string;
|
||||
added: Entity[];
|
||||
removed: Entity[];
|
||||
type: 'delta';
|
||||
};
|
||||
|
||||
export interface ProcessingStateManager {
|
||||
setProcessingItemResult(result: ProcessingItemResult): Promise<void>;
|
||||
getNextProcessingItem(): Promise<ProcessingItem>;
|
||||
replaceProcessingItems(request: ReplaceProcessingItemsRequest): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ Some features are not yet available for all supported Git providers.
|
||||
1. Install the Catalog Import Plugin:
|
||||
|
||||
```bash
|
||||
# packages/app
|
||||
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-catalog-import
|
||||
```
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"@types/react": "^16.9",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"js-base64": "^3.6.0",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@backstage/core": "^0.7.3",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@types/react": "^16.9",
|
||||
"lodash": "^4.17.15",
|
||||
"react": "^16.13.1",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
|
||||
@@ -13,11 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Entity, EntityRelation } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core';
|
||||
import { chunk, groupBy } from 'lodash';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '../api';
|
||||
|
||||
const BATCH_SIZE = 20;
|
||||
|
||||
export function useRelatedEntities(
|
||||
entity: Entity,
|
||||
{ type, kind }: { type?: string; kind?: string },
|
||||
@@ -40,16 +43,50 @@ export function useRelatedEntities(
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO: This code could be more efficient if there was an endpoint in the
|
||||
// backend that either returns the relations of entity (filtered by type)
|
||||
// or if there is a way to perform a batch request by entity name. However,
|
||||
// such an implementation would probably be better placed in the graphql API.
|
||||
const results = await Promise.all(
|
||||
relations?.map(r => catalogApi.getEntityByName(r.target)),
|
||||
// Group the relations by kind and namespace to reduce the size of the request query string.
|
||||
// Without this grouping, the kind and namespace would need to be specified for each relation, e.g.
|
||||
// `filter=kind=component,namespace=default,name=example1&filter=kind=component,namespace=default,name=example2`
|
||||
// with grouping, we can generate a query a string like
|
||||
// `filter=kind=component,namespace=default,name=example1,example2`
|
||||
const relationsByKindAndNamespace: EntityRelation[][] = Object.values(
|
||||
groupBy(relations, ({ target }) => {
|
||||
return `${target.kind}:${target.namespace}`.toLowerCase();
|
||||
}),
|
||||
);
|
||||
// Skip entities that where not found, for example if a relation references
|
||||
// an entity that doesn't exist.
|
||||
return results.filter(e => e) as Entity[];
|
||||
|
||||
// Split the names within each group into batches to further reduce the query string length.
|
||||
const batchedRelationsByKindAndNamespace: {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
nameBatches: string[][];
|
||||
}[] = [];
|
||||
for (const rs of relationsByKindAndNamespace) {
|
||||
batchedRelationsByKindAndNamespace.push({
|
||||
// All relations in a group have the same kind and namespace, so its arbitrary which we pick
|
||||
kind: rs[0].target.kind,
|
||||
namespace: rs[0].target.namespace,
|
||||
nameBatches: chunk(
|
||||
rs.map(r => r.target.name),
|
||||
BATCH_SIZE,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
batchedRelationsByKindAndNamespace.flatMap(rs => {
|
||||
return rs.nameBatches.map(names => {
|
||||
return catalogApi.getEntities({
|
||||
filter: {
|
||||
kind: rs.kind,
|
||||
'metadata.namespace': rs.namespace,
|
||||
'metadata.name': names,
|
||||
},
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return results.flatMap(r => r.items);
|
||||
}, [entity, type]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -91,14 +91,18 @@ describe('<HasComponentsCard />', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
catalogApi.getEntities.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
|
||||
@@ -91,14 +91,18 @@ describe('<HasSubcomponentsCard />', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
catalogApi.getEntities.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
|
||||
@@ -89,14 +89,18 @@ describe('<HasSystemsCard />', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'System',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
catalogApi.getEntities.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'System',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
|
||||
+18
-16
@@ -7,34 +7,35 @@ Website: [https://circleci.com/](https://circleci.com/)
|
||||
|
||||
## Setup
|
||||
|
||||
1. If you have standalone app (you didn't clone this repo), then do
|
||||
1. If you have a standalone app (you didn't clone this repo), then do
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-circleci
|
||||
```
|
||||
|
||||
2. Add the `EntityCircleCIContent` extension to the entity page in the app:
|
||||
2. Add the `EntityCircleCIContent` extension to the entity page in your app:
|
||||
|
||||
```tsx
|
||||
// packages/app/src/components/catalog/EntityPage.tsx
|
||||
import { EntityCircleCIContent } from '@backstage/plugin-circleci';
|
||||
// In packages/app/src/components/catalog/EntityPage.tsx
|
||||
import {
|
||||
EntityCircleCIContent,
|
||||
isCircleCIAvailable,
|
||||
} from '@backstage/plugin-circleci';
|
||||
|
||||
// ...
|
||||
const serviceEntityPage = (
|
||||
<EntityPageLayout>
|
||||
...
|
||||
<EntityLayout.Route path="/circle-ci" title="Circle CI">
|
||||
// For example in the CI/CD section
|
||||
const cicdContent = (
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isCircleCIAvailable}>
|
||||
<EntityCircleCIContent />
|
||||
</EntityLayout.Route>
|
||||
...
|
||||
</EntityPageLayout>
|
||||
);
|
||||
</EntitySwitch.Case>
|
||||
```
|
||||
|
||||
4. Add proxy config:
|
||||
|
||||
```yaml
|
||||
// app-config.yaml
|
||||
# In app-config.yaml
|
||||
proxy:
|
||||
'/circleci/api':
|
||||
target: https://circleci.com/api/v1.1
|
||||
@@ -42,10 +43,11 @@ proxy:
|
||||
Circle-Token: ${CIRCLECI_AUTH_TOKEN}
|
||||
```
|
||||
|
||||
5. Get and provide `CIRCLECI_AUTH_TOKEN` as env variable (https://circleci.com/docs/api/#add-an-api-token)
|
||||
6. Add `circleci.com/project-slug` annotation to your catalog-info.yaml file in format <git-provider>/<owner>/<project> (https://backstage.io/docs/architecture-decisions/adrs-adr002#format)
|
||||
5. Get and provide a `CIRCLECI_AUTH_TOKEN` as an environment variable (see the [CircleCI docs](https://circleci.com/docs/api/#add-an-api-token)).
|
||||
6. Add a `circleci.com/project-slug` annotation to your respective `catalog-info.yaml` files, on the format <git-provider>/<owner>/<project> (https://backstage.io/docs/architecture-decisions/adrs-adr002#format).
|
||||
|
||||
```yaml
|
||||
# Example catalog-info.yaml entity definition file
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
|
||||
@@ -16,6 +16,8 @@ Learn more with the Backstage blog post [New Cost Insights plugin: The engineer'
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-cost-insights
|
||||
```
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ Cost Explorer permission policy:
|
||||
Install the AWS Cost Explorer SDK. The AWS docs recommend using the SDK over making calls to the API directly as it simplifies authentication and provides direct access to commands.
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @aws-sdk/client-cost-explorer
|
||||
```
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ The FOSSA Plugin displays code statistics from [FOSSA](https://fossa.com/).
|
||||
1. Install the FOSSA Plugin:
|
||||
|
||||
```bash
|
||||
# packages/app
|
||||
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-fossa
|
||||
```
|
||||
|
||||
|
||||
@@ -11,15 +11,15 @@ TBD
|
||||
### Generic Requirements
|
||||
|
||||
1. Provide OAuth credentials:
|
||||
1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) with callback URL set to `http://localhost:7000/api/auth/github`.
|
||||
2. Take Client ID and Client Secret from the newly created app's settings page and put them into `AUTH_GITHUB_CLIENT_ID` and `AUTH_GITHUB_CLIENT_SECRET` env variables.
|
||||
1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) with the callback URL set to `http://localhost:7000/api/auth/github`.
|
||||
2. Take the Client ID and Client Secret from the newly created app's settings page and put them into `AUTH_GITHUB_CLIENT_ID` and `AUTH_GITHUB_CLIENT_SECRET` environment variables.
|
||||
2. Annotate your component with a correct GitHub Actions repository and owner:
|
||||
|
||||
The annotation key is `github.com/project-slug`.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
@@ -38,6 +38,7 @@ TBD
|
||||
1. Install the plugin dependency in your Backstage app package:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-github-actions
|
||||
```
|
||||
@@ -45,22 +46,24 @@ yarn add @backstage/plugin-github-actions
|
||||
2. Add to the app `EntityPage` component:
|
||||
|
||||
```tsx
|
||||
// packages/app/src/components/catalog/EntityPage.tsx
|
||||
import { EntityGithubActionsContent } from '@backstage/plugin-github-actions';
|
||||
// In packages/app/src/components/catalog/EntityPage.tsx
|
||||
import {
|
||||
EntityGithubActionsContent,
|
||||
isGithubActionsAvailable,
|
||||
} from '@backstage/plugin-github-actions';
|
||||
|
||||
// ...
|
||||
// You can add the tab to any number of pages, the service page is shown as an
|
||||
// example here
|
||||
const serviceEntityPage = (
|
||||
<EntityPageLayout>
|
||||
...
|
||||
<EntityLayout>
|
||||
{/* other tabs... */}
|
||||
<EntityLayout.Route path="/github-actions" title="GitHub Actions">
|
||||
<EntityGithubActionsContent />
|
||||
</EntityLayout.Route>
|
||||
...
|
||||
</EntityPageLayout>
|
||||
);
|
||||
```
|
||||
|
||||
2. Run the app with `yarn start` and the backend with `yarn --cwd packages/backend start`, navigate to `/github-actions/`.
|
||||
3. Run the app with `yarn start` and the backend with `yarn start-backend`.
|
||||
Then navigate to `/github-actions/` under any entity.
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"moment": "^2.27.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
|
||||
@@ -202,13 +202,13 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.head_commit.message}</TableCell>
|
||||
<TableCell>{details.value?.head_commit?.message}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.head_commit.id}</TableCell>
|
||||
<TableCell>{details.value?.head_commit?.id}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
@@ -231,7 +231,7 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
|
||||
<TableCell>
|
||||
<Typography noWrap>Author</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{`${details.value?.head_commit.author?.name} (${details.value?.head_commit.author?.email})`}</TableCell>
|
||||
<TableCell>{`${details.value?.head_commit?.author?.name} (${details.value?.head_commit?.author?.email})`}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
|
||||
@@ -13,8 +13,8 @@ The GitHub Deployments Plugin displays recent deployments from GitHub.
|
||||
1. Install the GitHub Deployments Plugin.
|
||||
|
||||
```bash
|
||||
# packages/app
|
||||
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-github-deployments
|
||||
```
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ By exposing GraphiQL as a plugin instead of a standalone app, it's possible to p
|
||||
Start out by installing the plugin in your Backstage app:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-graphiql
|
||||
```
|
||||
|
||||
+23
-17
@@ -11,25 +11,25 @@ Website: [https://jenkins.io/](https://jenkins.io/)
|
||||
1. If you have a standalone app (you didn't clone this repo), then do
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-jenkins
|
||||
```
|
||||
|
||||
2. Add the `EntityJenkinsContent` extension to the entity page in the app:
|
||||
|
||||
```tsx
|
||||
// packages/app/src/components/catalog/EntityPage.tsx
|
||||
import { EntityJenkinsContent } from '@backstage/plugin-circleci';
|
||||
// In packages/app/src/components/catalog/EntityPage.tsx
|
||||
import { EntityJenkinsContent } from '@backstage/plugin-jenkins';
|
||||
|
||||
// ...
|
||||
// You can add the tab to any number of pages, the service page is shown as an
|
||||
// example here
|
||||
const serviceEntityPage = (
|
||||
<EntityPageLayout>
|
||||
...
|
||||
<EntityLayout>
|
||||
{/* other tabs... */}
|
||||
<EntityLayout.Route path="/jenkins" title="Jenkins">
|
||||
<EntityJenkinsContent />
|
||||
</EntityLayout.Route>
|
||||
...
|
||||
</EntityPageLayout>
|
||||
);
|
||||
```
|
||||
|
||||
3. Add proxy configuration to `app-config.yaml`
|
||||
@@ -43,14 +43,18 @@ proxy:
|
||||
Authorization: Basic ${JENKINS_BASIC_AUTH_HEADER}
|
||||
```
|
||||
|
||||
4. Add an environment variable which contains the Jenkins credentials, (note: use an API token not your password). Here user is the name of the user created in Jenkins.
|
||||
4. Add an environment variable which contains the Jenkins credentials (NOTE:
|
||||
use an API token, not your password). Here `user` is the name of the user
|
||||
created in Jenkins.
|
||||
|
||||
```shell
|
||||
export JENKINS_BASIC_AUTH_HEADER=$(echo -n user:api-token | base64)
|
||||
```
|
||||
|
||||
5. Run app with `yarn start`
|
||||
6. Add the Jenkins folder annotation to your `catalog-info.yaml`, (note: currently this plugin only supports folders and Git SCM)
|
||||
5. Run the app with `yarn start`
|
||||
|
||||
6. Add the Jenkins folder annotation to your `catalog-info.yaml`, (NOTE:
|
||||
currently this plugin only supports folders and Git SCM)
|
||||
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -68,11 +72,11 @@ spec:
|
||||
|
||||
7. Register your component
|
||||
|
||||
8. Click the component in the catalog you should now see Jenkins builds, and a last build result for your master build.
|
||||
8. Click the component in the catalog. You should now see Jenkins builds, and a
|
||||
last build result for your master build.
|
||||
|
||||
Note:
|
||||
|
||||
If you are not using environment variable then you can directly type API token in app-config.yaml
|
||||
Note: If you are not using environment variables, you can directly type the API
|
||||
token into `app-config.yaml`.
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
@@ -83,7 +87,8 @@ proxy:
|
||||
Authorization: Basic YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw==
|
||||
```
|
||||
|
||||
YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw== is the base64 of user and it's API token e.g. admin:11ec256e438501c3f5c76b751a7e47af83
|
||||
The string starting with `YWR...` is the base64 encoding of the user and their
|
||||
API token, e.g. `admin:11ec256e438501c3f5c76b751a7e47af83`.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -94,4 +99,5 @@ YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw== is the base64 of user a
|
||||
## Limitations
|
||||
|
||||
- Only works with organization folder projects backed by GitHub
|
||||
- No pagination support currently, limited to 50 projects - don't run this on a Jenkins with lots of builds
|
||||
- No pagination support currently, limited to 50 projects - don't run this on a
|
||||
Jenkins instance with lots of builds
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Kafka Backend
|
||||
|
||||
This is the backend part of the Kafka plugin. It responds to Kafka requests from the frontend.
|
||||
This is the backend part of the Kafka plugin. It responds to Kafka requests
|
||||
from the frontend.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -16,7 +17,9 @@ A list of the brokers' host names and ports to connect to.
|
||||
|
||||
### `ssl` (optional)
|
||||
|
||||
Configure TLS connection to the Kafka cluster. The options are passed directly to [tls.connect] and used to create the TLS secure context. Normally these would include `key` and `cert`.
|
||||
Configure TLS connection to the Kafka cluster. The options are passed directly
|
||||
to [tls.connect] and used to create the TLS secure context. Normally these
|
||||
would include `key` and `cert`.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -39,7 +42,7 @@ kafka:
|
||||
clusters:
|
||||
- name: prod
|
||||
brokers:
|
||||
- my-cluser:9092
|
||||
- my-cluster:9092
|
||||
ssl: true
|
||||
sasl:
|
||||
mechanism: plain # or 'scram-sha-256' or 'scram-sha-512'
|
||||
|
||||
+15
-23
@@ -7,7 +7,9 @@
|
||||
1. Run:
|
||||
|
||||
```bash
|
||||
yarn add @backstage/plugin-kafka @backstage/plugin-kafka-backend
|
||||
# From your Backstage root directory
|
||||
yarn --cwd packages/app add @backstage/plugin-kafka
|
||||
yarn --cwd packages/backend add @backstage/plugin-kafka-backend
|
||||
```
|
||||
|
||||
2. Add the plugin backend:
|
||||
@@ -29,41 +31,31 @@ export default async function createPlugin({
|
||||
And then add to `packages/backend/src/index.ts`:
|
||||
|
||||
```js
|
||||
// ...
|
||||
// In packages/backend/src/index.ts
|
||||
import kafka from './plugins/kafka';
|
||||
// ...
|
||||
async function main() {
|
||||
// ...
|
||||
const kafkaEnv = useHotMemoize(module, () => createEnv('kafka'));
|
||||
// ...
|
||||
|
||||
const apiRouter = Router();
|
||||
// ...
|
||||
apiRouter.use('/kafka', await kafka(kafkaEnv));
|
||||
// ...
|
||||
```
|
||||
|
||||
3. Add the plugin frontend to `packages/app/src/plugin.ts`:
|
||||
|
||||
```js
|
||||
export { plugin as Kafka } from '@backstage/plugin-kafka';
|
||||
```
|
||||
|
||||
4. Register the plugin frontend router in `packages/app/src/components/catalog/EntityPage.tsx`:
|
||||
3. Add the plugin as a tab to your service entities:
|
||||
|
||||
```jsx
|
||||
import { Router as KafkaRouter } from '@backstage/plugin-kafka';
|
||||
// In packages/app/src/components/catalog/EntityPage.tsx
|
||||
import { EntityKafkaContent } from '@backstage/plugin-kafka';
|
||||
|
||||
// Then, somewhere inside <EntityPageLayout>
|
||||
|
||||
<EntityPageLayout.Content
|
||||
path="/kafka/*"
|
||||
title="Kafka"
|
||||
element={<KafkaRouter entity={entity} />}
|
||||
/>;
|
||||
const serviceEntityPage = (
|
||||
<EntityLayout>
|
||||
{/* other tabs... */}
|
||||
<EntityLayout.Route path="/kafka" title="Kafka">
|
||||
<EntityKafkaContent />
|
||||
</EntityLayout.Route>
|
||||
```
|
||||
|
||||
5. Add broker configs for the backend in your `app-config.yaml` (see
|
||||
4. Add broker configs for the backend in your `app-config.yaml` (see
|
||||
[kafka-backend](https://github.com/backstage/backstage/blob/master/plugins/kafka-backend/README.md)
|
||||
for more options):
|
||||
|
||||
@@ -76,7 +68,7 @@ kafka:
|
||||
- localhost:9092
|
||||
```
|
||||
|
||||
6. Add `kafka.apache.org/consumer-groups` annotation to your services:
|
||||
5. Add the `kafka.apache.org/consumer-groups` annotation to your services:
|
||||
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@backstage/backend-common": "^0.6.3",
|
||||
"@backstage/catalog-model": "^0.7.7",
|
||||
"@backstage/config": "^0.1.4",
|
||||
"@backstage/plugin-kubernetes-common": "^0.1.0",
|
||||
"@google-cloud/container": "^2.2.0",
|
||||
"@kubernetes/client-node": "^0.14.0",
|
||||
"@types/express": "^4.17.6",
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
*/
|
||||
|
||||
import { KubernetesAuthTranslator } from './types';
|
||||
import { KubernetesRequestBody, ClusterDetails } from '../types/types';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
export class GoogleKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator {
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
*/
|
||||
|
||||
import { KubernetesAuthTranslator } from './types';
|
||||
import { KubernetesRequestBody, ClusterDetails } from '../types/types';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
export class ServiceAccountKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator {
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { KubernetesRequestBody, ClusterDetails } from '../types/types';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
export interface KubernetesAuthTranslator {
|
||||
decorateClusterDetailsWithAuth(
|
||||
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
CustomResource,
|
||||
KubernetesFetcher,
|
||||
KubernetesObjectTypes,
|
||||
KubernetesRequestBody,
|
||||
KubernetesServiceLocator,
|
||||
} from '../types/types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types';
|
||||
import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator';
|
||||
|
||||
|
||||
@@ -32,15 +32,17 @@ import lodash, { Dictionary } from 'lodash';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
ClusterDetails,
|
||||
FetchResponse,
|
||||
FetchResponseWrapper,
|
||||
KubernetesErrorTypes,
|
||||
KubernetesFetcher,
|
||||
KubernetesFetchError,
|
||||
KubernetesObjectTypes,
|
||||
ObjectFetchParams,
|
||||
CustomResource,
|
||||
} from '../types/types';
|
||||
import {
|
||||
FetchResponse,
|
||||
KubernetesFetchError,
|
||||
KubernetesErrorTypes,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
|
||||
export interface Clients {
|
||||
|
||||
@@ -23,11 +23,11 @@ import { MultiTenantServiceLocator } from '../service-locator/MultiTenantService
|
||||
import {
|
||||
ClusterDetails,
|
||||
KubernetesClustersSupplier,
|
||||
KubernetesRequestBody,
|
||||
KubernetesServiceLocator,
|
||||
ServiceLocatorMethod,
|
||||
CustomResource,
|
||||
} from '../types/types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
import { KubernetesFanOutHandler } from './KubernetesFanOutHandler';
|
||||
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
|
||||
@@ -14,146 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ExtensionsV1beta1Ingress,
|
||||
V1ConfigMap,
|
||||
V1Deployment,
|
||||
V1HorizontalPodAutoscaler,
|
||||
V1Pod,
|
||||
V1ReplicaSet,
|
||||
V1Service,
|
||||
} from '@kubernetes/client-node';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import type {
|
||||
FetchResponse,
|
||||
KubernetesFetchError,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
export interface ClusterDetails {
|
||||
name: string;
|
||||
url: string;
|
||||
authProvider: string;
|
||||
serviceAccountToken?: string | undefined;
|
||||
skipTLSVerify?: boolean;
|
||||
}
|
||||
|
||||
export interface KubernetesRequestBody {
|
||||
auth?: {
|
||||
google?: string;
|
||||
};
|
||||
entity: Entity;
|
||||
}
|
||||
|
||||
export interface ClusterObjects {
|
||||
cluster: { name: string };
|
||||
resources: FetchResponse[];
|
||||
errors: KubernetesFetchError[];
|
||||
}
|
||||
|
||||
export interface ObjectsByEntityResponse {
|
||||
items: ClusterObjects[];
|
||||
}
|
||||
|
||||
export interface FetchResponseWrapper {
|
||||
errors: KubernetesFetchError[];
|
||||
responses: FetchResponse[];
|
||||
}
|
||||
|
||||
export type FetchResponse =
|
||||
| PodFetchResponse
|
||||
| ServiceFetchResponse
|
||||
| ConfigMapFetchResponse
|
||||
| DeploymentFetchResponse
|
||||
| ReplicaSetsFetchResponse
|
||||
| HorizontalPodAutoscalersFetchResponse
|
||||
| IngressesFetchResponse
|
||||
| CustomResourceFetchResponse;
|
||||
|
||||
// TODO fairly sure there's a easier way to do this
|
||||
|
||||
export type KubernetesObjectTypes =
|
||||
| 'pods'
|
||||
| 'services'
|
||||
| 'configmaps'
|
||||
| 'deployments'
|
||||
| 'replicasets'
|
||||
| 'horizontalpodautoscalers'
|
||||
| 'ingresses'
|
||||
| 'customresources';
|
||||
|
||||
export interface PodFetchResponse {
|
||||
type: 'pods';
|
||||
resources: Array<V1Pod>;
|
||||
}
|
||||
|
||||
export interface ServiceFetchResponse {
|
||||
type: 'services';
|
||||
resources: Array<V1Service>;
|
||||
}
|
||||
|
||||
export interface ConfigMapFetchResponse {
|
||||
type: 'configmaps';
|
||||
resources: Array<V1ConfigMap>;
|
||||
}
|
||||
|
||||
export interface DeploymentFetchResponse {
|
||||
type: 'deployments';
|
||||
resources: Array<V1Deployment>;
|
||||
}
|
||||
|
||||
export interface ReplicaSetsFetchResponse {
|
||||
type: 'replicasets';
|
||||
resources: Array<V1ReplicaSet>;
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscalersFetchResponse {
|
||||
type: 'horizontalpodautoscalers';
|
||||
resources: Array<V1HorizontalPodAutoscaler>;
|
||||
}
|
||||
|
||||
export interface IngressesFetchResponse {
|
||||
type: 'ingresses';
|
||||
resources: Array<ExtensionsV1beta1Ingress>;
|
||||
}
|
||||
|
||||
export interface CustomResourceFetchResponse {
|
||||
type: 'customresources';
|
||||
resources: Array<any>;
|
||||
}
|
||||
|
||||
export interface ObjectFetchParams {
|
||||
serviceId: string;
|
||||
clusterDetails: ClusterDetails;
|
||||
objectTypesToFetch: Set<KubernetesObjectTypes>;
|
||||
labelSelector: string;
|
||||
customResources: CustomResource[];
|
||||
}
|
||||
|
||||
// Fetches information from a kubernetes cluster using the cluster details object
|
||||
// to target a specific cluster
|
||||
export interface KubernetesFetcher {
|
||||
fetchObjectsForService(
|
||||
params: ObjectFetchParams,
|
||||
): Promise<FetchResponseWrapper>;
|
||||
}
|
||||
|
||||
// Used to locate which cluster(s) a service is running on
|
||||
export interface KubernetesServiceLocator {
|
||||
getClustersByServiceId(serviceId: string): Promise<ClusterDetails[]>;
|
||||
}
|
||||
|
||||
// Used to load cluster details from different sources
|
||||
export interface KubernetesClustersSupplier {
|
||||
getClusters(): Promise<ClusterDetails[]>;
|
||||
}
|
||||
|
||||
export type KubernetesErrorTypes =
|
||||
| 'BAD_REQUEST'
|
||||
| 'UNAUTHORIZED_ERROR'
|
||||
| 'SYSTEM_ERROR'
|
||||
| 'UNKNOWN_ERROR';
|
||||
|
||||
export interface KubernetesFetchError {
|
||||
errorType: KubernetesErrorTypes;
|
||||
statusCode?: number;
|
||||
resourcePath?: string;
|
||||
}
|
||||
export type ClusterLocatorMethod =
|
||||
| ConfigClusterLocatorMethod
|
||||
| GKEClusterLocatorMethod;
|
||||
|
||||
export interface ConfigClusterLocatorMethod {
|
||||
/**
|
||||
@@ -195,15 +63,61 @@ export interface GKEClusterLocatorMethod {
|
||||
region?: string;
|
||||
}
|
||||
|
||||
export type ClusterLocatorMethod =
|
||||
| ConfigClusterLocatorMethod
|
||||
| GKEClusterLocatorMethod;
|
||||
|
||||
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
|
||||
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws';
|
||||
|
||||
export interface CustomResource {
|
||||
group: string;
|
||||
apiVersion: string;
|
||||
plural: string;
|
||||
}
|
||||
|
||||
export interface ObjectFetchParams {
|
||||
serviceId: string;
|
||||
clusterDetails: ClusterDetails;
|
||||
objectTypesToFetch: Set<KubernetesObjectTypes>;
|
||||
labelSelector: string;
|
||||
customResources: CustomResource[];
|
||||
}
|
||||
|
||||
// Fetches information from a kubernetes cluster using the cluster details object
|
||||
// to target a specific cluster
|
||||
export interface KubernetesFetcher {
|
||||
fetchObjectsForService(
|
||||
params: ObjectFetchParams,
|
||||
): Promise<FetchResponseWrapper>;
|
||||
}
|
||||
|
||||
export interface FetchResponseWrapper {
|
||||
errors: KubernetesFetchError[];
|
||||
responses: FetchResponse[];
|
||||
}
|
||||
|
||||
// TODO fairly sure there's a easier way to do this
|
||||
|
||||
export type KubernetesObjectTypes =
|
||||
| 'pods'
|
||||
| 'services'
|
||||
| 'configmaps'
|
||||
| 'deployments'
|
||||
| 'replicasets'
|
||||
| 'horizontalpodautoscalers'
|
||||
| 'ingresses'
|
||||
| 'customresources';
|
||||
|
||||
// Used to load cluster details from different sources
|
||||
export interface KubernetesClustersSupplier {
|
||||
getClusters(): Promise<ClusterDetails[]>;
|
||||
}
|
||||
|
||||
// Used to locate which cluster(s) a service is running on
|
||||
export interface KubernetesServiceLocator {
|
||||
getClustersByServiceId(serviceId: string): Promise<ClusterDetails[]>;
|
||||
}
|
||||
|
||||
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
|
||||
|
||||
export interface ClusterDetails {
|
||||
name: string;
|
||||
url: string;
|
||||
authProvider: string;
|
||||
serviceAccountToken?: string | undefined;
|
||||
skipTLSVerify?: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
# @backstage/plugin-kubernetes-common
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Adds types to be shared by the backend and the front end.
|
||||
@@ -0,0 +1,3 @@
|
||||
# @backstage/plugin-kubernetes-common
|
||||
|
||||
Common types and functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user