Merge branch 'master' of https://github.com/spotify/backstage into lintMod
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
---
|
||||
'@backstage/cli': minor
|
||||
---
|
||||
|
||||
We've bumped the `@eslint-typescript` packages to the latest, which now add some additional rules that might cause lint failures.
|
||||
The main one which could become an issue is the [no-use-before-define](https://eslint.org/docs/rules/no-use-before-define) rule.
|
||||
|
||||
Every plugin and app has the ability to override these rules if you want to ignore them for now.
|
||||
|
||||
You can reset back to the default behaviour by using the following in your own `.eslint.js`
|
||||
|
||||
```js
|
||||
rules: {
|
||||
'no-use-before-define': 'off'
|
||||
}
|
||||
```
|
||||
|
||||
Because of the nature of this change, we're unable to provide a grace period for the update :(
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': patch
|
||||
---
|
||||
|
||||
Allow expand functionality to top panel product chart tooltip.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
Add className to the SidebarItem
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Add `--lax` option to `config:print` and `config:check`, which causes all environment variables to be assumed to be set.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-model': patch
|
||||
---
|
||||
|
||||
Adds a new optional `links` metadata field to the Entity class within the `catalog-model` package (as discussed in [[RFC] Entity Links](https://github.com/backstage/backstage/issues/3787)). This PR adds support for the entity links only. Follow up PR's will introduce the UI component to display them.
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
'@backstage/config-loader': patch
|
||||
---
|
||||
|
||||
Added support for environment variable substitutions in string configuration values using a `${VAR}` placeholder. All environment variables must be available, or the entire expression will be evaluated to `undefined`. To escape a substitution, use `$${...}`, which will end up as `${...}`.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
app:
|
||||
baseUrl: https://${BASE_HOST}
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
---
|
||||
|
||||
Improve error reporting for plugin misconfiguration.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Use .text instead of .json for ALB key response
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-model': patch
|
||||
---
|
||||
|
||||
Replace `yup` with `ajv`, for validation of catalog entities.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/config-loader': minor
|
||||
---
|
||||
|
||||
Removed support for the deprecated `$data` placeholder.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/config-loader': patch
|
||||
---
|
||||
|
||||
Bump `config-loader` to `ajv` 7, to enable v7 feature use elsewhere
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Properly forward errors that occur when looking up GitLab project IDs.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-model': patch
|
||||
---
|
||||
|
||||
Introduce json schema variants of the `yup` validation schemas
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
Add rate limiter for concurrent execution of file uploads in AWS and Google publishers
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/config-loader': minor
|
||||
---
|
||||
|
||||
Enable further processing of configuration files included using the `$include` placeholder. Meaning that for example for example `$env` includes will be processed as usual in included files.
|
||||
@@ -0,0 +1,110 @@
|
||||
name: Tugboat E2E Tests
|
||||
on: deployment_status
|
||||
jobs:
|
||||
set-pending:
|
||||
if: github.event.deployment_status.state != 'success' && github.event.deployment_status.state != 'failed'
|
||||
name: Set pending waiting for Tugboat
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Set an initial commit status message to indicate that the tests are
|
||||
# running.
|
||||
- name: set pending status
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
|
||||
debug: true
|
||||
script: |
|
||||
return github.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.sha,
|
||||
state: 'pending',
|
||||
context: 'Backstage Tugboat E2E Tests',
|
||||
description: 'Waiting for Tugboat to complete deployment',
|
||||
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
|
||||
});
|
||||
|
||||
run-tests:
|
||||
# Only run after a successful Tugboat deployment.
|
||||
if: github.event.deployment_status.state == 'success'
|
||||
name: Run tests against Tugboat deployment
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Set an initial commit status message to indicate that the tests are
|
||||
# running.
|
||||
- name: set pending status
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
|
||||
debug: true
|
||||
script: |
|
||||
return github.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.sha,
|
||||
state: 'pending',
|
||||
context: 'Backstage Tugboat E2E Tests',
|
||||
description: 'Running against tugboat preview',
|
||||
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
|
||||
});
|
||||
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
# This is required because the environment_url param that Tugboat uses
|
||||
# to tell us where the preview is located isn't supported unless you
|
||||
# specify the custom Accept header when getting the deployment_status,
|
||||
# and GitHub actions doesn't do that by default. So instead we have to
|
||||
# load the status object manually and get the data we need.
|
||||
# https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/
|
||||
- name: get deployment status
|
||||
id: get-status-env
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
|
||||
result-encoding: string
|
||||
script: |
|
||||
const result = await github.repos.getDeploymentStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
deployment_id: context.payload.deployment.id,
|
||||
status_id: context.payload.deployment_status.id,
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.ant-man-preview+json'
|
||||
},
|
||||
});
|
||||
console.log(result);
|
||||
return result.data.environment_url;
|
||||
- name: echo tugboat preview url
|
||||
run: |
|
||||
curl ${{steps.get-status-env.outputs.result}}
|
||||
- name: set status
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
|
||||
script: |
|
||||
return github.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.sha,
|
||||
state: "error",
|
||||
context: 'Backstage Tugboat E2E Tests',
|
||||
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
|
||||
});
|
||||
- name: set status
|
||||
if: ${{ success() }}
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
|
||||
script: |
|
||||
return github.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.sha,
|
||||
state: "success",
|
||||
context: 'Backstage Tugboat E2E Tests',
|
||||
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
backstage:
|
||||
image: tugboatqa/node:lts
|
||||
expose: 7000
|
||||
default: true
|
||||
commands:
|
||||
init:
|
||||
- mkdir -p /etc/service/node
|
||||
- echo "#!/bin/sh" > /etc/service/node/run
|
||||
- echo "yarn --cwd ${TUGBOAT_ROOT} start-backend --config ${TUGBOAT_ROOT}/app-config.yaml --config ${TUGBOAT_ROOT}/.tugboat/tugboat.app-config.production.yaml" >> /etc/service/node/run
|
||||
- chmod +x /etc/service/node/run
|
||||
build:
|
||||
- yarn workspace example-app build
|
||||
update:
|
||||
- yarn install
|
||||
@@ -0,0 +1,13 @@
|
||||
app:
|
||||
title: Backstage Tugboat Preview
|
||||
baseUrl:
|
||||
$env: TUGBOAT_DEFAULT_SERVICE_URL
|
||||
|
||||
backend:
|
||||
baseUrl:
|
||||
$env: TUGBOAT_DEFAULT_SERVICE_URL
|
||||
cors:
|
||||
origin:
|
||||
$env: TUGBOAT_DEFAULT_SERVICE_URL
|
||||
methods: [GET, POST, PUT, DELETE]
|
||||
credentials: true
|
||||
+7
-5
@@ -99,6 +99,13 @@ kubernetes:
|
||||
- 'config'
|
||||
clusters: []
|
||||
|
||||
kafka:
|
||||
clientId: backstage
|
||||
clusters:
|
||||
- name: cluster
|
||||
brokers:
|
||||
- localhost:9092
|
||||
|
||||
integrations:
|
||||
github:
|
||||
- host: github.com
|
||||
@@ -372,8 +379,3 @@ homepage:
|
||||
timezone: 'Asia/Tokyo'
|
||||
pagerduty:
|
||||
eventsBaseUrl: 'https://events.pagerduty.com/v2'
|
||||
|
||||
kafka:
|
||||
clientId: backstage
|
||||
brokers:
|
||||
- localhost:9092
|
||||
|
||||
@@ -4,8 +4,6 @@ title: ADR000: [TITLE]
|
||||
description: Architecture Decision Record (ADR) for [TITLE] [DESCRIPTION]
|
||||
---
|
||||
|
||||
# ADR000: [title]
|
||||
|
||||
<!-- These documents have names that are short noun phrases. For example, "ADR001: Deployment on Ruby on Rails 3.0.10" or "ADR009: LDAP for Multitenant Integration" -->
|
||||
|
||||
## Context
|
||||
|
||||
@@ -4,8 +4,6 @@ title: ADR010: Use the Luxon Date Library
|
||||
description: Architecture Decision Record (ADR) for Luxon Date Library
|
||||
---
|
||||
|
||||
# ADR010: Use the Luxon Date Library
|
||||
|
||||
## Context
|
||||
|
||||
Date formatting (e.g. `a day ago`) and calculations are common within Backstage.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
id: adrs-adr011
|
||||
title: ADR011: Plugin Package Structure
|
||||
description: Architecture Decision Record (ADR) for Plugin Package Structure
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
A core feature of Backstage is the extensibility via plugins. The Backstage
|
||||
repository is open for contributions of plugins. Even most of the core features
|
||||
are implemented as plugins. A plugin consists of one or multiple packages in the
|
||||
`plugins/` directory. Up till now, we have a simple conventions for naming
|
||||
plugin packages: Plugins are named `x`, with the option of having a related
|
||||
backend plugin called `x-backend` (where `x` is the plugin name, like `catalog`
|
||||
or `techdocs`). There is a need for sharing code between the frontend and
|
||||
backend of a plugin, between backend plugins, or components and hooks between
|
||||
different frontend plugins
|
||||
([some examples](https://github.com/backstage/backstage/issues/3655#issuecomment-758166746)).
|
||||
This results in emerging plugin packages with shared code, like
|
||||
`packages/catalog-client` or `packages/techdocs-common`.
|
||||
|
||||
> There is a common phrase in software development:
|
||||
> [Naming things is hard](https://martinfowler.com/bliki/TwoHardThings.html)
|
||||
|
||||
To keep the contributed plugins consistent, this Architecture Decision Record
|
||||
provides rules for naming plugin packages.
|
||||
|
||||
## Decision
|
||||
|
||||
We will place all plugin related code in the `plugins/` directory. The
|
||||
`packages/` directory is reserved for core package of Backstage.
|
||||
|
||||
We follow this structure for plugin packages (where `x` is the plugin name, for
|
||||
example `catalog` or `techdocs`):
|
||||
|
||||
- `x`: Contains the main frontend code of the plugin.
|
||||
- `x-backend`: Contains the main backend code of the plugin.
|
||||
- `x-react`: Contains shared widgets, hooks and similar that both the plugin
|
||||
itself (`x`) and third-party frontend plugins can depend on.
|
||||
- `x-node`: Contains utilities for backends that both the plugin backend itself
|
||||
(`x-backend`) and third-party backend plugins can depend on.
|
||||
- `x-common`: An isomorphic package with platform agnostic models, clients, and
|
||||
utilities that all packages above or any third-party plugin package can depend
|
||||
on.
|
||||
|
||||
We prefix the package names with `@backstage/plugin-`.
|
||||
|
||||
This structure is based on a
|
||||
[suggestion in issue #3655](https://github.com/backstage/backstage/issues/3655#issuecomment-758166746).
|
||||
|
||||
## Consequences
|
||||
|
||||
We will actively migrate existing packages that are part of a plugin to the
|
||||
`plugins/` folder. This affects packages like:
|
||||
|
||||
- `packages/techdocs-common` which should be moved to `plugins/techdocs-node`
|
||||
and named `@backstage/plugin-techdocs-node`.
|
||||
- `packages/catalog-client` which will be part of a future
|
||||
`plugins/catalog-common` and named `@backstage/plugin-catalog-common`.
|
||||
- While the new location of `packages/catalog-model` should be
|
||||
`plugins/catalog-common` we might want to do an exception here, as it's a very
|
||||
central package.
|
||||
|
||||
The limited set of rules might not be sufficient in the future. If additional
|
||||
packages are required, we will revisit this decision and extend the pattern.
|
||||
|
||||
If possible, we will add tools, such as lint rules, to help enforce the package
|
||||
names and dependencies between them or CLI commands to generate these packages.
|
||||
|
||||
The distinction between core packages and plugins helps us to setup
|
||||
[CODEOWNERS](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners)
|
||||
in the repository. We can set the code owners for the `packages/` folder to the
|
||||
core team and create additional rules (like `plugins/x*`) for plugin
|
||||
maintainers.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -0,0 +1,130 @@
|
||||
---
|
||||
id: configuration
|
||||
title: Configuring Kubernetes integration
|
||||
sidebar_label: Configuration
|
||||
# prettier-ignore
|
||||
description: Configuring the Kubernetes integration for Backstage expose your entity's objects
|
||||
---
|
||||
|
||||
Configuring the Backstage Kubernetes integration involves two steps:
|
||||
|
||||
1. Enabling the backend to collect objects from your Kubernetes cluster(s).
|
||||
2. Surfacing your Kubernetes objects in catalog entities
|
||||
|
||||
## Configuring Kubernetes Clusters
|
||||
|
||||
The following is a full example entry in `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
kubernetes:
|
||||
serviceLocatorMethod: 'multiTenant'
|
||||
clusterLocatorMethods:
|
||||
- 'config'
|
||||
clusters:
|
||||
- url: http://127.0.0.1:9999
|
||||
name: minikube
|
||||
authProvider: 'serviceAccount'
|
||||
serviceAccountToken:
|
||||
$env: K8S_MINIKUBE_TOKEN
|
||||
- url: http://127.0.0.2:9999
|
||||
name: gke-cluster-1
|
||||
authProvider: 'google'
|
||||
```
|
||||
|
||||
### `serviceLocatorMethod`
|
||||
|
||||
This configures how to determine which clusters a component is running in.
|
||||
|
||||
Currently, the only valid value is:
|
||||
|
||||
- `multiTenant` - This configuration assumes that all components run on all the
|
||||
provided clusters.
|
||||
|
||||
### `clusterLocatorMethods`
|
||||
|
||||
This is an array used to determine where to retrieve cluster configuration from.
|
||||
|
||||
Currently, the only valid cluster locator method is:
|
||||
|
||||
- `config` - This cluster locator method will read cluster information from your
|
||||
app-config (see below).
|
||||
|
||||
### `clusters`
|
||||
|
||||
Used by the `config` cluster locator method to construct Kubernetes clients.
|
||||
|
||||
### `clusters.\*.url`
|
||||
|
||||
The base URL to the Kubernetes control plane. Can be found by using the
|
||||
"Kubernetes master" result from running the `kubectl cluster-info` command.
|
||||
|
||||
### `clusters.\*.name`
|
||||
|
||||
A name to represent this cluster, this must be unique within the `clusters`
|
||||
array. Users will see this value in the Service Catalog Kubernetes plugin.
|
||||
|
||||
### `clusters.\*.authProvider`
|
||||
|
||||
This determines how the Kubernetes client authenticates with the Kubernetes
|
||||
cluster. Valid values are:
|
||||
|
||||
| Value | Description |
|
||||
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. |
|
||||
| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. |
|
||||
|
||||
### `clusters.\*.serviceAccount` (optional)
|
||||
|
||||
The service account token to be used when using the `serviceAccount` auth
|
||||
provider.
|
||||
|
||||
### Role Based Access Control
|
||||
|
||||
The current RBAC permissions required are read-only cluster wide, for the
|
||||
following objects:
|
||||
|
||||
- pods
|
||||
- services
|
||||
- configmaps
|
||||
- deployments
|
||||
- replicasets
|
||||
- horizontalpodautoscalers
|
||||
- ingresses
|
||||
|
||||
## Surfacing your Kubernetes components as part of an entity
|
||||
|
||||
There are two ways to surface your Kubernetes components as part of an entity.
|
||||
The label selector takes precedence over the annotation/service id.
|
||||
|
||||
### Common `backstage.io/kubernetes-id` label
|
||||
|
||||
#### Adding the entity annotation
|
||||
|
||||
In order for Backstage to detect that an entity has Kubernetes components, the
|
||||
following annotation should be added to the entity's `catalog-info.yaml`:
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
```
|
||||
|
||||
#### Labeling Kubernetes components
|
||||
|
||||
In order for Kubernetes components to show up in the service catalog as a part
|
||||
of an entity, Kubernetes components themselves can have the following label:
|
||||
|
||||
```yaml
|
||||
'backstage.io/kubernetes-id': <BACKSTAGE_ENTITY_NAME>
|
||||
```
|
||||
|
||||
### Label selector query annotation
|
||||
|
||||
You can write your own custom label selector query that Backstage will use to
|
||||
lookup the objects (similar to `kubectl --selector="your query here"`). Review
|
||||
the
|
||||
[labels and selectors Kubernetes documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
|
||||
for more info.
|
||||
|
||||
```yaml
|
||||
'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end'
|
||||
```
|
||||
@@ -5,123 +5,26 @@ sidebar_label: Overview
|
||||
description: Monitoring Kubernetes based services with the service catalog
|
||||
---
|
||||
|
||||
Kubernetes in Backstage is a way to monitor your service's current status when
|
||||
it is deployed on Kubernetes.
|
||||
Kubernetes in Backstage is a tool that's designed around the needs of service
|
||||
owners, not cluster admins. Now developers can easily check the health of their
|
||||
services no matter how or where those services are deployed — whether it's on a
|
||||
local host for testing or in production on dozens of clusters around the world.
|
||||
|
||||
## Configuration
|
||||
It will elevate the visibility of errors where identified, and provide drill
|
||||
down about the deployments, pods, and other objects for a service.
|
||||
|
||||
Example:
|
||||

|
||||
|
||||
```yaml
|
||||
kubernetes:
|
||||
serviceLocatorMethod: 'multiTenant'
|
||||
clusterLocatorMethods:
|
||||
- 'config'
|
||||
clusters:
|
||||
- url: http://127.0.0.1:9999
|
||||
name: minikube
|
||||
authProvider: 'serviceAccount'
|
||||
serviceAccountToken:
|
||||
$env: K8S_MINIKUBE_TOKEN
|
||||
- url: http://127.0.0.2:9999
|
||||
name: gke-cluster-1
|
||||
authProvider: 'google'
|
||||
```
|
||||
The feature is made up of two plugins:
|
||||
[`@backstage/plugin-kubernetes`](https://github.com/backstage/backstage/tree/master/plugins/kubernetes)
|
||||
and
|
||||
[`@backstage/plugin-kubernetes-backend`](https://github.com/backstage/backstage/tree/master/plugins/kubernetes-backend).
|
||||
|
||||
### serviceLocatorMethod
|
||||
The frontend plugin exposes information to the end user in a digestible way,
|
||||
while the backend wraps the mechanics to connect to Kubernetes clusters to
|
||||
collect the relevant information.
|
||||
|
||||
This configures how to determine which clusters a component is running in.
|
||||
## Let's use it!
|
||||
|
||||
Currently, the only valid value is:
|
||||
|
||||
- `multiTenant` - This configuration assumes that all components run on all the
|
||||
provided clusters.
|
||||
|
||||
### clusterLocatorMethods
|
||||
|
||||
This is an array used to determine where to retrieve cluster configuration from.
|
||||
|
||||
Currently, the only valid cluster locator method is:
|
||||
|
||||
- `config` - This cluster locator method will read cluster information from your
|
||||
app-config (see below).
|
||||
|
||||
### clusters
|
||||
|
||||
Used by the `config` cluster locator method to construct Kubernetes clients.
|
||||
|
||||
### clusters.\*.url
|
||||
|
||||
The base URL to the Kubernetes control plane. Can be found by using the
|
||||
"Kubernetes master" result from running the `kubectl cluster-info` command.
|
||||
|
||||
### clusters.\*.name
|
||||
|
||||
A name to represent this cluster, this must be unique within the `clusters`
|
||||
array. Users will see this value in the Service Catalog Kubernetes plugin.
|
||||
|
||||
### clusters.\*.authProvider
|
||||
|
||||
This determines how the Kubernetes client authenticates with the Kubernetes
|
||||
cluster. Valid values are:
|
||||
|
||||
| Value | Description |
|
||||
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. |
|
||||
| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. |
|
||||
|
||||
### clusters.\*.serviceAccount (optional)
|
||||
|
||||
The service account token to be used when using the `serviceAccount` auth
|
||||
provider.
|
||||
|
||||
## Role Based Access Control
|
||||
|
||||
The current RBAC permissions required are read-only cluster wide, for the
|
||||
following objects:
|
||||
|
||||
- pods
|
||||
- services
|
||||
- configmaps
|
||||
- deployments
|
||||
- replicasets
|
||||
- horizontalpodautoscalers
|
||||
- ingresses
|
||||
|
||||
## Surfacing your Kubernetes components as part of an entity
|
||||
|
||||
There are two ways to surface your Kubernetes components as part of an entity.
|
||||
The label selector takes precedence over the annotation/service id.
|
||||
|
||||
### Common `backstage.io/kubernetes-id` label
|
||||
|
||||
#### Adding the entity annotation
|
||||
|
||||
In order for Backstage to detect that an entity has Kubernetes components, the
|
||||
following annotation should be added to the entity's `catalog-info.yaml`:
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
```
|
||||
|
||||
#### Labeling Kubernetes components
|
||||
|
||||
In order for Kubernetes components to show up in the service catalog as a part
|
||||
of an entity, Kubernetes components themselves can have the following label:
|
||||
|
||||
```yaml
|
||||
'backstage.io/kubernetes-id': <BACKSTAGE_ENTITY_NAME>
|
||||
```
|
||||
|
||||
### Label selector query annotation
|
||||
|
||||
You can write your own custom label selector query that Backstage will use to
|
||||
lookup the objects (similar to `kubectl --selector="your query here"`). Review
|
||||
the
|
||||
[labels and selectors Kubernetes documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
|
||||
for more info.
|
||||
|
||||
```yaml
|
||||
'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end'
|
||||
```
|
||||
To get started, first you must [install the Kubernetes plugins](installation.md)
|
||||
and then [configure them](configuration.md).
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
id: installation
|
||||
title: Installation
|
||||
description: Installing Kubernetes plugin into Backstage
|
||||
---
|
||||
|
||||
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, start
|
||||
[here](../../getting-started/index.md).
|
||||
|
||||
## 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
|
||||
your `packages/app` directory, and install the `@backstage/plugin-kubernetes`
|
||||
package.
|
||||
|
||||
```bash
|
||||
cd my-backstage-app/
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-kubernetes
|
||||
```
|
||||
|
||||
Once the package has been installed, you need to import the plugin in your app.
|
||||
Add the following to `packages/app/src/plugins.ts`:
|
||||
|
||||
`plugins.ts`:
|
||||
|
||||
```typescript
|
||||
export { plugin as Kubernetes } from '@backstage/plugin-kubernetes';
|
||||
```
|
||||
|
||||
Now, add 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`:
|
||||
|
||||
```tsx
|
||||
import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes';
|
||||
|
||||
// ...
|
||||
|
||||
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
// ...
|
||||
<EntityPageLayout.Content
|
||||
path="/kubernetes/*"
|
||||
title="Kubernetes"
|
||||
element={<KubernetesRouter entity={entity} />}
|
||||
/>
|
||||
// ...
|
||||
</EntityPageLayout>
|
||||
);
|
||||
```
|
||||
|
||||
That's it! But now, we need the Kubernetes Backend plugin for the frontend to
|
||||
work.
|
||||
|
||||
## Adding Kubernetes Backend plugin
|
||||
|
||||
Navigate to `packages/backend` of your Backstage app, and install the
|
||||
`@backstage/plugin-kubernetes-backend` package.
|
||||
|
||||
```bash
|
||||
cd my-backstage-app/
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
import { createRouter } from '@backstage/plugin-kubernetes-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config });
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
import kubernetes from './plugins/kubernetes';
|
||||
|
||||
// ...
|
||||
|
||||
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
|
||||
Backstage app.
|
||||
|
||||
## Running Backstage locally
|
||||
|
||||
Start the frontend and the backend app by
|
||||
[running backstage locally](../../getting-started/running-backstage-locally.md).
|
||||
|
||||
## Configuration
|
||||
|
||||
After installing the plugins in the code, you'll need to then
|
||||
[configure them](configuration.md).
|
||||
@@ -54,6 +54,11 @@ software catalog API.
|
||||
"labels": {
|
||||
"system": "public-websites"
|
||||
},
|
||||
"links": [{
|
||||
"url": "https://admin.example-org.com",
|
||||
"title": "Admin Dashboard",
|
||||
"icon": "dashboard"
|
||||
}],
|
||||
"tags": ["java"],
|
||||
"name": "artist-web",
|
||||
"uid": "2152f463-549d-4d8d-a94d-ce2b7676c6e2"
|
||||
@@ -81,6 +86,10 @@ metadata:
|
||||
circleci.com/project-slug: github/example-org/artist-website
|
||||
tags:
|
||||
- java
|
||||
links:
|
||||
- url: https://admin.example-org.com
|
||||
title: Admin Dashboard
|
||||
icon: dashboard
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: production
|
||||
@@ -314,6 +323,34 @@ This field is optional, and currently has no special semantics.
|
||||
Each tag must be sequences of `[a-z0-9]` separated by `-`, at most 63 characters
|
||||
in total.
|
||||
|
||||
### `links` [optional]
|
||||
|
||||
A list of external hyperlinks related to the entity. Links can provide
|
||||
additional contextual information that may be located outside of Backstage
|
||||
itself. For example, an admin dashboard or external CMS page.
|
||||
|
||||
Users may add links to descriptor YAML files to provide additional reference
|
||||
information to external content & resources. Links are not intended to drive any
|
||||
additional functionality within Backstage, which is best left to `annotations`
|
||||
and `labels`. It is recommended to use links only when an equivalent well-known
|
||||
`annotation` does not cover a similar use case.
|
||||
|
||||
Fields of a link are:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------- | ------ | ------------------------------------------------------------------------------------ |
|
||||
| `url` | String | [Required] A `url` in a standard `uri` format (e.g. `https://example.com/some/page`) |
|
||||
| `title` | String | [Optional] A user friendly display name for the link. |
|
||||
| `icon` | String | [Optional] A key representing a visual icon to be displayed in the UI. |
|
||||
|
||||
_NOTE_: The `icon` field value is meant to be a semantic key that will map to a
|
||||
specific icon that may be provided by an icon library (e.g. `material-ui`
|
||||
icons). These keys should be a sequence of `[a-z0-9A-Z]`, possibly separated by
|
||||
one of `[-_.]`. Backstage may support some basic icons out of the box, but the
|
||||
Backstage integrator will ultimately be left to provide the appropriate icon
|
||||
component mappings. A generic fallback icon would be provided if a mapping
|
||||
cannot be resolved.
|
||||
|
||||
## Common to All Kinds: Relations
|
||||
|
||||
The `relations` root field is a read-only list of relations, between the current
|
||||
|
||||
@@ -173,7 +173,12 @@ and access to a running Docker daemon. You can create a GitHub access token
|
||||
docs on creating private GitHub access tokens is available
|
||||
[here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
Note that the need for private GitHub access tokens will be replaced with GitHub
|
||||
Apps integration further down the line.
|
||||
Apps integration further down the line by using the existing `integrations`
|
||||
config.
|
||||
|
||||
> Note: Some of this configuration may already be set up as part of your
|
||||
> `app-config.yaml`. We're moving away from the duplicated config for
|
||||
> authentication in the `scaffolder` section and using `integrations` instead.
|
||||
|
||||
#### GitHub
|
||||
|
||||
@@ -187,10 +192,14 @@ by specifying `visibility` option. Valid options are `public`, `private` and
|
||||
public within the enterprise.
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
github:
|
||||
- host: github.com
|
||||
token:
|
||||
$env: GITHUB_TOKEN
|
||||
|
||||
scaffolder:
|
||||
github:
|
||||
token:
|
||||
$env: GITHUB_TOKEN
|
||||
visibility: public # or 'internal' or 'private'
|
||||
```
|
||||
|
||||
@@ -201,10 +210,9 @@ allows to configure the private access token and the base URL of a GitLab
|
||||
instance:
|
||||
|
||||
```yaml
|
||||
scaffolder:
|
||||
integrations:
|
||||
gitlab:
|
||||
api:
|
||||
baseUrl: https://gitlab.com
|
||||
- host: gitlab.com
|
||||
token:
|
||||
$env: GITLAB_TOKEN
|
||||
```
|
||||
@@ -218,10 +226,9 @@ will hopefully support on-prem installations as well but that has not been
|
||||
verified.
|
||||
|
||||
```yaml
|
||||
scaffolder:
|
||||
integrations:
|
||||
azure:
|
||||
baseUrl: https://dev.azure.com/{your-organization}
|
||||
api:
|
||||
- host: dev.azure.com
|
||||
token:
|
||||
$env: AZURE_TOKEN
|
||||
```
|
||||
|
||||
@@ -4,19 +4,97 @@ title: Other
|
||||
description: Documentation on different ways of Deployment
|
||||
---
|
||||
|
||||
## Deploying Locally
|
||||
## Docker
|
||||
|
||||
### Try on Docker
|
||||
Here we have an example Dockerfile that you can use to build everything together
|
||||
in one container. This Dockerfile uses multi-stage builds, and a
|
||||
`backend:bundle` command from the CLI.
|
||||
|
||||
Run the following commands if you have Docker environment
|
||||
It also provides caching on the `yarn install`'s so that you don't have to do it
|
||||
unless absolutely necessary.
|
||||
|
||||
```bash
|
||||
$ yarn install
|
||||
$ yarn docker-build
|
||||
$ docker run --rm -it -p 7000:7000 -e NODE_ENV=development example-backend:latest
|
||||
> Note: This Dockerfile assumes that you're running SQLite, or your
|
||||
> configuration is setup to connect to an external PostgreSQL Database.
|
||||
|
||||
```Dockerfile
|
||||
# Stage 1 - Create yarn install skeleton layer
|
||||
FROM node:14-buster AS packages
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
COPY packages packages
|
||||
|
||||
# Uncomment this line if you have a local plugins folder
|
||||
# COPY plugins plugins
|
||||
|
||||
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||
|
||||
# Stage 2 - Install dependencies and build packages
|
||||
FROM node:14-buster AS build
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=packages /app .
|
||||
|
||||
RUN yarn install --network-timeout 600000 && rm -rf "$(yarn cache dir)"
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn tsc
|
||||
RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies
|
||||
|
||||
# Stage 3 - Build the actual backend image and install production dependencies
|
||||
FROM node:14-buster
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy from build stage
|
||||
COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./
|
||||
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
|
||||
|
||||
RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)"
|
||||
|
||||
COPY --from=build /app/packages/backend/dist/bundle.tar.gz .
|
||||
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
|
||||
|
||||
COPY app-config.yaml app-config.production.yaml ./
|
||||
|
||||
CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"]
|
||||
```
|
||||
|
||||
Then open http://localhost:7000 on your browser.
|
||||
Before building you should also include a `.dockerignore`. This will greatly
|
||||
improve the context boot up time of Docker as we are no longer sending all of
|
||||
the `node_modules` into the context. It also helps us avoid some limitations and
|
||||
errors that may occur when trying to share the `node_modules` folder to inside
|
||||
the build.
|
||||
|
||||
You can add the following contents to the root of your repository at
|
||||
`.dockerignore` and it might look something like the following:
|
||||
|
||||
```dockerignore
|
||||
.git
|
||||
node_modules
|
||||
packages/*/node_modules
|
||||
plugins/*/node_modules
|
||||
plugins/*/dist
|
||||
```
|
||||
|
||||
Once you have added both the `Dockerfile` and `.dockerignore` to the root of
|
||||
your project, and run the following to build the container under a specified
|
||||
tag.
|
||||
|
||||
```sh
|
||||
$ docker build -t example-deployment .
|
||||
```
|
||||
|
||||
To run the image locally you can run:
|
||||
|
||||
```sh
|
||||
$ docker run -p -it 7000:7000 example-deployment
|
||||
```
|
||||
|
||||
You should then start to get logs in your terminal, and then you can open your
|
||||
browser at `http://localhost:7000`
|
||||
|
||||
## Heroku
|
||||
|
||||
|
||||
@@ -39,7 +39,11 @@
|
||||
{
|
||||
"type": "subcategory",
|
||||
"label": "Kubernetes",
|
||||
"ids": ["features/kubernetes/overview"]
|
||||
"ids": [
|
||||
"features/kubernetes/overview",
|
||||
"features/kubernetes/installation",
|
||||
"features/kubernetes/configuration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "subcategory",
|
||||
@@ -184,7 +188,8 @@
|
||||
"architecture-decisions/adrs-adr007",
|
||||
"architecture-decisions/adrs-adr008",
|
||||
"architecture-decisions/adrs-adr009",
|
||||
"architecture-decisions/adrs-adr010"
|
||||
"architecture-decisions/adrs-adr010",
|
||||
"architecture-decisions/adrs-adr011"
|
||||
],
|
||||
"Contribute": ["../CONTRIBUTING"],
|
||||
"Support": ["support/support", "support/project-structure"],
|
||||
|
||||
@@ -119,6 +119,7 @@ nav:
|
||||
- ADR008 - Default Catalog File Name: 'architecture-decisions/adr008-default-catalog-file-name.md'
|
||||
- ADR009 - Entity References: 'architecture-decisions/adr009-entity-references.md'
|
||||
- ADR010 - Luxon Date Library: 'architecture-decisions/adr010-luxon-date-library.md'
|
||||
- ADR011 - Plugin Package Structure: 'architecture-decisions/adr011-plugin-package-structure.md'
|
||||
- Contribute: '../CONTRIBUTING.md'
|
||||
- Support:
|
||||
- 'support/support.md'
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# example-app
|
||||
|
||||
## 0.2.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [681111228]
|
||||
- Updated dependencies [12a56cdfe]
|
||||
- Updated dependencies [8b7ef9f8b]
|
||||
- Updated dependencies [fac91bcc5]
|
||||
- Updated dependencies [9dd057662]
|
||||
- Updated dependencies [234e7d985]
|
||||
- Updated dependencies [ef7957be4]
|
||||
- Updated dependencies [0b1182346]
|
||||
- Updated dependencies [a6e3b9596]
|
||||
- @backstage/plugin-kubernetes@0.3.7
|
||||
- @backstage/cli@0.5.0
|
||||
- @backstage/plugin-cost-insights@0.6.0
|
||||
- @backstage/plugin-catalog@0.2.14
|
||||
- @backstage/plugin-catalog-import@0.3.6
|
||||
- @backstage/plugin-scaffolder@0.4.1
|
||||
- @backstage/plugin-kafka@0.2.0
|
||||
|
||||
## 0.2.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.2.12",
|
||||
"version": "0.2.13",
|
||||
"private": true,
|
||||
"bundled": true,
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.7.0",
|
||||
"@backstage/cli": "^0.4.7",
|
||||
"@backstage/cli": "^0.5.0",
|
||||
"@backstage/core": "^0.5.0",
|
||||
"@backstage/plugin-api-docs": "^0.4.3",
|
||||
"@backstage/plugin-catalog": "^0.2.12",
|
||||
"@backstage/plugin-catalog-import": "^0.3.5",
|
||||
"@backstage/plugin-catalog": "^0.2.14",
|
||||
"@backstage/plugin-catalog-import": "^0.3.6",
|
||||
"@backstage/plugin-circleci": "^0.2.6",
|
||||
"@backstage/plugin-cloudbuild": "^0.2.7",
|
||||
"@backstage/plugin-cost-insights": "^0.5.7",
|
||||
"@backstage/plugin-cost-insights": "^0.6.0",
|
||||
"@backstage/plugin-explore": "^0.2.3",
|
||||
"@backstage/plugin-gcp-projects": "^0.2.3",
|
||||
"@backstage/plugin-github-actions": "^0.3.0",
|
||||
@@ -20,14 +20,14 @@
|
||||
"@backstage/plugin-graphiql": "^0.2.6",
|
||||
"@backstage/plugin-org": "^0.3.4",
|
||||
"@backstage/plugin-jenkins": "^0.3.6",
|
||||
"@backstage/plugin-kafka": "^0.1.1",
|
||||
"@backstage/plugin-kubernetes": "^0.3.6",
|
||||
"@backstage/plugin-kafka": "^0.2.0",
|
||||
"@backstage/plugin-kubernetes": "^0.3.7",
|
||||
"@backstage/plugin-lighthouse": "^0.2.8",
|
||||
"@backstage/plugin-newrelic": "^0.2.3",
|
||||
"@backstage/plugin-pagerduty": "0.2.6",
|
||||
"@backstage/plugin-register-component": "^0.2.7",
|
||||
"@backstage/plugin-rollbar": "^0.2.8",
|
||||
"@backstage/plugin-scaffolder": "^0.4.0",
|
||||
"@backstage/plugin-scaffolder": "^0.4.1",
|
||||
"@backstage/plugin-sentry": "^0.3.3",
|
||||
"@backstage/plugin-search": "^0.2.6",
|
||||
"@backstage/plugin-tech-radar": "^0.3.3",
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# @backstage/backend-common
|
||||
|
||||
## 0.5.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 26a3a6cf0: Honor the branch ref in the url when cloning.
|
||||
|
||||
This fixes a bug in the scaffolder prepare stage where a non-default branch
|
||||
was specified in the scaffolder URL but the default branch was cloned.
|
||||
For example, even though the `other` branch is specified in this example, the
|
||||
`master` branch was actually cloned:
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
locations:
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/other/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
|
||||
```
|
||||
|
||||
This also fixes a 404 in the prepare stage for GitLab URLs.
|
||||
|
||||
- 664dd08c9: URL Reader's readTree: Fix bug with github.com URLs.
|
||||
- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref.
|
||||
- Updated dependencies [6800da78d]
|
||||
- Updated dependencies [9dd057662]
|
||||
- Updated dependencies [ef7957be4]
|
||||
- Updated dependencies [ef7957be4]
|
||||
- Updated dependencies [ef7957be4]
|
||||
- @backstage/integration@0.3.1
|
||||
- @backstage/config-loader@0.5.0
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-common",
|
||||
"description": "Common functionality library for Backstage backends",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"private": false,
|
||||
@@ -31,8 +31,8 @@
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "^0.1.1",
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/config-loader": "^0.4.1",
|
||||
"@backstage/integration": "^0.3.0",
|
||||
"@backstage/config-loader": "^0.5.0",
|
||||
"@backstage/integration": "^0.3.1",
|
||||
"@types/cors": "^2.8.6",
|
||||
"@types/express": "^4.17.6",
|
||||
"archiver": "^5.0.2",
|
||||
@@ -43,7 +43,7 @@
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.1",
|
||||
"git-url-parse": "^11.4.3",
|
||||
"git-url-parse": "^11.4.4",
|
||||
"helmet": "^4.0.0",
|
||||
"isomorphic-git": "^1.8.0",
|
||||
"knex": "^0.21.6",
|
||||
@@ -66,7 +66,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.4.7",
|
||||
"@backstage/cli": "^0.5.0",
|
||||
"@backstage/test-utils": "^0.1.5",
|
||||
"@types/archiver": "^5.1.0",
|
||||
"@types/compression": "^1.7.0",
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import path from 'path';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
@@ -31,6 +33,8 @@ const treeResponseFactory = ReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
|
||||
|
||||
describe('AzureUrlReader', () => {
|
||||
const worker = setupServer();
|
||||
msw.setupDefaultHandlers(worker);
|
||||
@@ -139,6 +143,16 @@ describe('AzureUrlReader', () => {
|
||||
});
|
||||
|
||||
describe('readTree', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[tmpDir]: mockFs.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
const repoBuffer = fs.readFileSync(
|
||||
path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'),
|
||||
);
|
||||
@@ -200,6 +214,21 @@ describe('AzureUrlReader', () => {
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files', async () => {
|
||||
const response = await processor.readTree(
|
||||
'https://dev.azure.com/organization/project/_git/repository',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: tmpDir });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
).resolves.toBe('site_name: Test\n');
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('throws a NotModifiedError when given a etag in options', async () => {
|
||||
const fnAzure = async () => {
|
||||
await processor.readTree(
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
import fs from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import path from 'path';
|
||||
@@ -53,6 +54,16 @@ describe('BitbucketUrlReader', () => {
|
||||
});
|
||||
|
||||
describe('readTree', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'/tmp': mockFs.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
const worker = setupServer();
|
||||
msw.setupDefaultHandlers(worker);
|
||||
|
||||
@@ -126,12 +137,12 @@ describe('BitbucketUrlReader', () => {
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/repositories/backstage/mock/commits/some-branch',
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -155,6 +166,21 @@ describe('BitbucketUrlReader', () => {
|
||||
expect(mkDocsFile.toString()).toBe('site_name: Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: '/tmp' });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
).resolves.toBe('site_name: Test\n');
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('uses private bitbucket host', async () => {
|
||||
const response = await hostedBitbucketProcessor.readTree(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch',
|
||||
@@ -185,6 +211,18 @@ describe('BitbucketUrlReader', () => {
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files with a subpath', async () => {
|
||||
const response = await bitbucketProcessor.readTree(
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: '/tmp' });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('throws a NotModifiedError when given a etag in options', async () => {
|
||||
const fnBitbucket = async () => {
|
||||
await bitbucketProcessor.readTree(
|
||||
|
||||
@@ -121,31 +121,9 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// Get the filename of archive from the header of the response
|
||||
const contentDispositionHeader = archiveBitbucketResponse.headers.get(
|
||||
'content-disposition',
|
||||
) as string;
|
||||
if (!contentDispositionHeader) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. ` +
|
||||
'Bitbucket API response for downloading archive does not contain content-disposition header ',
|
||||
);
|
||||
}
|
||||
const fileNameRegEx = new RegExp(
|
||||
/^attachment; filename=(?<fileName>.*).zip$/,
|
||||
);
|
||||
const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
|
||||
?.groups?.fileName;
|
||||
if (!archiveFileName) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. Bitbucket API response for downloading archive has an unexpected ` +
|
||||
`format of content-disposition header ${contentDispositionHeader} `,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.treeResponseFactory.fromZipArchive({
|
||||
stream: (archiveBitbucketResponse.body as unknown) as Readable,
|
||||
path: `${archiveFileName}/${filepath}`,
|
||||
subpath: filepath,
|
||||
etag: lastCommitShortHash,
|
||||
filter: options?.filter,
|
||||
});
|
||||
@@ -161,13 +139,18 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
}
|
||||
|
||||
private async getLastCommitShortHash(url: string): Promise<string> {
|
||||
const { name: repoName, owner: project, ref } = parseGitUrl(url);
|
||||
const { resource, name: repoName, owner: project, ref } = parseGitUrl(url);
|
||||
|
||||
let branch = ref;
|
||||
if (!branch) {
|
||||
branch = await getBitbucketDefaultBranch(url, this.config);
|
||||
}
|
||||
const commitsApiUrl = `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`;
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222
|
||||
const commitsApiUrl = isHosted
|
||||
? `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`
|
||||
: `${this.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
|
||||
|
||||
const commitsResponse = await fetch(
|
||||
commitsApiUrl,
|
||||
@@ -182,14 +165,26 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
}
|
||||
|
||||
const commits = await commitsResponse.json();
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].hash
|
||||
) {
|
||||
return commits.values[0].hash.substring(0, 12);
|
||||
if (isHosted) {
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].hash
|
||||
) {
|
||||
return commits.values[0].hash.substring(0, 12);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
commits &&
|
||||
commits.values &&
|
||||
commits.values.length > 0 &&
|
||||
commits.values[0].id
|
||||
) {
|
||||
return commits.values[0].id.substring(0, 12);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to read response from ${commitsApiUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { GithubCredentialsProvider } from '@backstage/integration';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
import fs from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import path from 'path';
|
||||
@@ -107,6 +108,16 @@ describe('GithubUrlReader', () => {
|
||||
});
|
||||
|
||||
describe('readTree', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'/tmp': mockFs.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
const repoBuffer = fs.readFileSync(
|
||||
path.resolve(
|
||||
'src',
|
||||
@@ -227,6 +238,21 @@ describe('GithubUrlReader', () => {
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files', async () => {
|
||||
const response = await githubProcessor.readTree(
|
||||
'https://github.com/backstage/mock',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: '/tmp' });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
).resolves.toBe('site_name: Test\n');
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('should use the headers from the credentials provider to the fetch request', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
@@ -293,6 +319,18 @@ describe('GithubUrlReader', () => {
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files with subpath', async () => {
|
||||
const response = await githubProcessor.readTree(
|
||||
'https://github.com/backstage/mock/tree/main/docs',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: '/tmp' });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('throws a NotModifiedError when given a etag in options', async () => {
|
||||
const fnGithub = async () => {
|
||||
await githubProcessor.readTree('https://github.com/backstage/mock', {
|
||||
|
||||
@@ -166,37 +166,11 @@ export class GithubUrlReader implements UrlReader {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// Get the filename of archive from the header of the response
|
||||
const contentDispositionHeader = archive.headers.get(
|
||||
'content-disposition',
|
||||
) as string;
|
||||
if (!contentDispositionHeader) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. ` +
|
||||
'GitHub API response for downloading archive does not contain content-disposition header ',
|
||||
);
|
||||
}
|
||||
const fileNameRegEx = new RegExp(
|
||||
/^attachment; filename=(?<fileName>.*).tar.gz$/,
|
||||
);
|
||||
const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
|
||||
?.groups?.fileName;
|
||||
if (!archiveFileName) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. GitHub API response for downloading archive has an unexpected ` +
|
||||
`format of content-disposition header ${contentDispositionHeader} `,
|
||||
);
|
||||
}
|
||||
|
||||
// The path includes the name of the directory inside the tarball and a sub path
|
||||
// if requested in readTree.
|
||||
const path = `${archiveFileName}/${filepath}`;
|
||||
|
||||
return await this.deps.treeResponseFactory.fromTarArchive({
|
||||
// TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want
|
||||
// to stick to using that in exclusively backend code.
|
||||
stream: (archive.body as unknown) as Readable,
|
||||
path,
|
||||
subpath: filepath,
|
||||
etag: commitSha,
|
||||
filter: options?.filter,
|
||||
});
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
import fs from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import path from 'path';
|
||||
@@ -153,6 +154,16 @@ describe('GitlabUrlReader', () => {
|
||||
});
|
||||
|
||||
describe('readTree', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'/tmp': mockFs.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
const archiveBuffer = fs.readFileSync(
|
||||
path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'),
|
||||
);
|
||||
@@ -254,6 +265,21 @@ describe('GitlabUrlReader', () => {
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files', async () => {
|
||||
const response = await gitlabProcessor.readTree(
|
||||
'https://gitlab.com/backstage/mock',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: '/tmp' });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
).resolves.toBe('site_name: Test\n');
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('returns the wanted files from hosted gitlab', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
@@ -296,6 +322,18 @@ describe('GitlabUrlReader', () => {
|
||||
expect(indexMarkdownFile.toString()).toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('creates a directory with the wanted files with subpath', async () => {
|
||||
const response = await gitlabProcessor.readTree(
|
||||
'https://gitlab.com/backstage/mock/tree/main/docs',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: '/tmp' });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('throws a NotModifiedError when given a etag in options', async () => {
|
||||
const fnGitlab = async () => {
|
||||
await gitlabProcessor.readTree('https://gitlab.com/backstage/mock', {
|
||||
|
||||
@@ -140,33 +140,9 @@ export class GitlabUrlReader implements UrlReader {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// Get the filename of archive from the header of the response
|
||||
const contentDispositionHeader = archiveGitLabResponse.headers.get(
|
||||
'content-disposition',
|
||||
) as string;
|
||||
if (!contentDispositionHeader) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. ` +
|
||||
'GitLab API response for downloading archive does not contain content-disposition header ',
|
||||
);
|
||||
}
|
||||
const fileNameRegEx = new RegExp(
|
||||
/^attachment; filename="(?<fileName>.*).zip"$/,
|
||||
);
|
||||
const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
|
||||
?.groups?.fileName;
|
||||
if (!archiveFileName) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. GitLab API response for downloading archive has an unexpected ` +
|
||||
`format of content-disposition header ${contentDispositionHeader} `,
|
||||
);
|
||||
}
|
||||
|
||||
const path = filepath ? `${archiveFileName}/${filepath}/` : '';
|
||||
|
||||
return await this.treeResponseFactory.fromZipArchive({
|
||||
stream: (archiveGitLabResponse.body as unknown) as Readable,
|
||||
path,
|
||||
subpath: filepath,
|
||||
etag: commitSha,
|
||||
filter: options?.filter,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { getVoidLogger } from '../logging';
|
||||
import { UrlReaders } from './UrlReaders';
|
||||
|
||||
const reader = UrlReaders.default({
|
||||
logger: getVoidLogger(),
|
||||
config: new ConfigReader({
|
||||
// The tokens in this config provide read only access to the backstage-verification repos
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
token: `${86}af${617}d9c3c8bf958b37a${630691452765}bb0b0a`,
|
||||
},
|
||||
],
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.com',
|
||||
token: 'tveGtSHDBJM9ZRHZNRfm',
|
||||
},
|
||||
],
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
username: 'backstage-verification',
|
||||
appPassword: 'H79MAAhtbZwCafkVTrrQ',
|
||||
},
|
||||
],
|
||||
azure: [
|
||||
{
|
||||
host: 'dev.azure.com',
|
||||
// lasts until 2022-01-27
|
||||
token: 'bhs5cbukiuxrkc3ftuyt5h3eqewtkj37lmf3jx5aoajivq3f5jmq',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
function withRetries(count: number, fn: () => Promise<void>) {
|
||||
return async () => {
|
||||
let error;
|
||||
for (let i = 0; i < count; i++) {
|
||||
try {
|
||||
await fn();
|
||||
return;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
describe('UrlReaders', () => {
|
||||
it(
|
||||
'should read data from azure',
|
||||
withRetries(3, async () => {
|
||||
const data = await reader.read(
|
||||
'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2Ftemplate.yaml',
|
||||
);
|
||||
expect(data.toString()).toContain('test-template-azure');
|
||||
|
||||
const res = await reader.readTree(
|
||||
'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2F{{cookiecutter.name}}',
|
||||
);
|
||||
const files = await res.files();
|
||||
expect(files).toEqual([
|
||||
{
|
||||
path: 'catalog-info.yaml',
|
||||
content: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
it(
|
||||
'should read data from gitlab',
|
||||
withRetries(3, async () => {
|
||||
const data = await reader.read(
|
||||
'https://gitlab.com/backstage-verification/test-templates/-/blob/master/template.yaml',
|
||||
);
|
||||
expect(data.toString()).toContain('test-template-gitlab');
|
||||
|
||||
const res = await reader.readTree(
|
||||
'https://gitlab.com/backstage-verification/test-templates/-/tree/master/{{cookiecutter.name}}',
|
||||
);
|
||||
const files = await res.files();
|
||||
expect(files).toEqual([
|
||||
{
|
||||
path: 'catalog-info.yaml',
|
||||
content: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
it(
|
||||
'should read data from bitbucket',
|
||||
withRetries(3, async () => {
|
||||
const data = await reader.read(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
);
|
||||
expect(data.toString()).toContain('test-template-bitbucket');
|
||||
|
||||
const res = await reader.readTree(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/{{cookiecutter.name}}',
|
||||
);
|
||||
const files = await res.files();
|
||||
expect(files).toEqual([
|
||||
{
|
||||
path: 'catalog-info.yaml',
|
||||
content: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
it(
|
||||
'should read data from github',
|
||||
withRetries(3, async () => {
|
||||
const data = await reader.read(
|
||||
'https://github.com/backstage-verification/test-templates/blob/master/template.yaml',
|
||||
);
|
||||
expect(data.toString()).toContain('test-template-github');
|
||||
|
||||
const res = await reader.readTree(
|
||||
'https://github.com/backstage-verification/test-templates/tree/master/{{cookiecutter.name}}',
|
||||
);
|
||||
const files = await res.files();
|
||||
expect(files).toEqual([
|
||||
{
|
||||
path: 'catalog-info.yaml',
|
||||
content: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -24,8 +24,9 @@ import { ZipArchiveResponse } from './ZipArchiveResponse';
|
||||
type FromArchiveOptions = {
|
||||
// A binary stream of a tar archive.
|
||||
stream: Readable;
|
||||
// If set, the root of the tree will be set to the given directory path.
|
||||
path?: string;
|
||||
// If unset, the files at the root of the tree will be read.
|
||||
// subpath must not contain the name of the top level directory.
|
||||
subpath?: string;
|
||||
// etag of the blob
|
||||
etag: string;
|
||||
// Filter passed on from the ReadTreeOptions
|
||||
@@ -45,7 +46,7 @@ export class ReadTreeResponseFactory {
|
||||
async fromTarArchive(options: FromArchiveOptions): Promise<ReadTreeResponse> {
|
||||
return new TarArchiveResponse(
|
||||
options.stream,
|
||||
options.path ?? '',
|
||||
options.subpath ?? '',
|
||||
this.workDir,
|
||||
options.etag,
|
||||
options.filter,
|
||||
@@ -55,7 +56,7 @@ export class ReadTreeResponseFactory {
|
||||
async fromZipArchive(options: FromArchiveOptions): Promise<ReadTreeResponse> {
|
||||
return new ZipArchiveResponse(
|
||||
options.stream,
|
||||
options.path ?? '',
|
||||
options.subpath ?? '',
|
||||
this.workDir,
|
||||
options.etag,
|
||||
options.filter,
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('TarArchiveResponse', () => {
|
||||
it('should read files', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
|
||||
const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', 'etag');
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const files = await res.files();
|
||||
|
||||
expect(files).toEqual([
|
||||
@@ -61,12 +61,8 @@ describe('TarArchiveResponse', () => {
|
||||
it('should read files with filter', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
|
||||
const res = new TarArchiveResponse(
|
||||
stream,
|
||||
'mock-main/',
|
||||
'/tmp',
|
||||
'etag',
|
||||
path => path.endsWith('.yml'),
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag', path =>
|
||||
path.endsWith('.yml'),
|
||||
);
|
||||
const files = await res.files();
|
||||
|
||||
@@ -83,7 +79,7 @@ describe('TarArchiveResponse', () => {
|
||||
it('should read as archive and files', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
|
||||
const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', 'etag');
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const buffer = await res.archive();
|
||||
|
||||
await expect(res.archive()).rejects.toThrow(
|
||||
@@ -115,24 +111,18 @@ describe('TarArchiveResponse', () => {
|
||||
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const dir = await res.dir();
|
||||
|
||||
await expect(
|
||||
fs.readFile(resolvePath(dir, 'mock-main/mkdocs.yml'), 'utf8'),
|
||||
fs.readFile(resolvePath(dir, 'mkdocs.yml'), 'utf8'),
|
||||
).resolves.toBe('site_name: Test\n');
|
||||
await expect(
|
||||
fs.readFile(resolvePath(dir, 'mock-main/docs/index.md'), 'utf8'),
|
||||
fs.readFile(resolvePath(dir, 'docs/index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('should extract archive into directory with a subpath', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
|
||||
const res = new TarArchiveResponse(
|
||||
stream,
|
||||
'mock-main/docs/',
|
||||
'/tmp',
|
||||
'etag',
|
||||
);
|
||||
const res = new TarArchiveResponse(stream, 'docs', '/tmp', 'etag');
|
||||
const dir = await res.dir();
|
||||
|
||||
expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/);
|
||||
@@ -144,12 +134,8 @@ describe('TarArchiveResponse', () => {
|
||||
it('should extract archive into directory with a subpath and filter', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
|
||||
const res = new TarArchiveResponse(
|
||||
stream,
|
||||
'mock-main/',
|
||||
'/tmp',
|
||||
'etag',
|
||||
path => path.endsWith('.yml'),
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag', path =>
|
||||
path.endsWith('.yml'),
|
||||
);
|
||||
const dir = await res.dir({ targetDir: '/tmp' });
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ import {
|
||||
const TarParseStream = (Parse as unknown) as { new (): ParseStream };
|
||||
|
||||
const pipeline = promisify(pipelineCb);
|
||||
// Matches a directory name + one `/` at the start of any string,
|
||||
// containing any character except `/` one or more times, and ending with a `/`
|
||||
// e.g. Will match `dirA/` in `dirA/dirB/file.ext`
|
||||
const directoryNameRegex = /^[^\/]+\//;
|
||||
|
||||
/**
|
||||
* Wraps a tar archive stream into a tree response reader.
|
||||
@@ -78,14 +82,18 @@ export class TarArchiveResponse implements ReadTreeResponse {
|
||||
return;
|
||||
}
|
||||
|
||||
// File path relative to the root extracted directory. Will remove the
|
||||
// top level dir name from the path since its name is hard to predetermine.
|
||||
const relativePath = entry.path.replace(directoryNameRegex, '');
|
||||
|
||||
if (this.subPath) {
|
||||
if (!entry.path.startsWith(this.subPath)) {
|
||||
if (!relativePath.startsWith(this.subPath)) {
|
||||
entry.resume();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const path = entry.path.slice(this.subPath.length);
|
||||
const path = relativePath.slice(this.subPath.length);
|
||||
if (this.filter) {
|
||||
if (!this.filter(path)) {
|
||||
entry.resume();
|
||||
@@ -97,7 +105,10 @@ export class TarArchiveResponse implements ReadTreeResponse {
|
||||
await pipeline(entry, concatStream(resolve));
|
||||
});
|
||||
|
||||
files.push({ path, content: () => content });
|
||||
files.push({
|
||||
path,
|
||||
content: () => content,
|
||||
});
|
||||
|
||||
entry.resume();
|
||||
});
|
||||
@@ -138,7 +149,9 @@ export class TarArchiveResponse implements ReadTreeResponse {
|
||||
options?.targetDir ??
|
||||
(await fs.mkdtemp(platformPath.join(this.workDir, 'backstage-')));
|
||||
|
||||
const strip = this.subPath ? this.subPath.split('/').length - 1 : 0;
|
||||
// Equivalent of tar --strip-components=N
|
||||
// When no subPath is given, remove just 1 top level directory
|
||||
const strip = this.subPath ? this.subPath.split('/').length : 1;
|
||||
|
||||
await pipeline(
|
||||
this.stream,
|
||||
@@ -146,7 +159,10 @@ export class TarArchiveResponse implements ReadTreeResponse {
|
||||
strip,
|
||||
cwd: dir,
|
||||
filter: path => {
|
||||
if (this.subPath && !path.startsWith(this.subPath)) {
|
||||
// File path relative to the root extracted directory. Will remove the
|
||||
// top level dir name from the path since its name is hard to predetermine.
|
||||
const relativePath = path.replace(directoryNameRegex, '');
|
||||
if (this.subPath && !relativePath.startsWith(this.subPath)) {
|
||||
return false;
|
||||
}
|
||||
if (this.filter) {
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('ZipArchiveResponse', () => {
|
||||
it('should read files', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
|
||||
const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', 'etag');
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const files = await res.files();
|
||||
|
||||
expect(files).toEqual([
|
||||
@@ -61,12 +61,8 @@ describe('ZipArchiveResponse', () => {
|
||||
it('should read files with filter', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
|
||||
const res = new ZipArchiveResponse(
|
||||
stream,
|
||||
'mock-main/',
|
||||
'/tmp',
|
||||
'etag',
|
||||
path => path.endsWith('.yml'),
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag', path =>
|
||||
path.endsWith('.yml'),
|
||||
);
|
||||
const files = await res.files();
|
||||
|
||||
@@ -83,7 +79,7 @@ describe('ZipArchiveResponse', () => {
|
||||
it('should read as archive and files', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
|
||||
const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', 'etag');
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const buffer = await res.archive();
|
||||
|
||||
await expect(res.archive()).rejects.toThrow(
|
||||
@@ -117,22 +113,17 @@ describe('ZipArchiveResponse', () => {
|
||||
const dir = await res.dir();
|
||||
|
||||
await expect(
|
||||
fs.readFile(resolvePath(dir, 'mock-main/mkdocs.yml'), 'utf8'),
|
||||
fs.readFile(resolvePath(dir, 'mkdocs.yml'), 'utf8'),
|
||||
).resolves.toBe('site_name: Test\n');
|
||||
await expect(
|
||||
fs.readFile(resolvePath(dir, 'mock-main/docs/index.md'), 'utf8'),
|
||||
fs.readFile(resolvePath(dir, 'docs/index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
});
|
||||
|
||||
it('should extract archive into directory with a subpath', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
|
||||
const res = new ZipArchiveResponse(
|
||||
stream,
|
||||
'mock-main/docs/',
|
||||
'/tmp',
|
||||
'etag',
|
||||
);
|
||||
const res = new ZipArchiveResponse(stream, 'docs/', '/tmp', 'etag');
|
||||
const dir = await res.dir();
|
||||
|
||||
expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/);
|
||||
@@ -144,12 +135,8 @@ describe('ZipArchiveResponse', () => {
|
||||
it('should extract archive into directory with a subpath and filter', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
|
||||
const res = new ZipArchiveResponse(
|
||||
stream,
|
||||
'mock-main/',
|
||||
'/tmp',
|
||||
'etag',
|
||||
path => path.endsWith('.yml'),
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag', path =>
|
||||
path.endsWith('.yml'),
|
||||
);
|
||||
const dir = await res.dir({ targetDir: '/tmp' });
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ import {
|
||||
ReadTreeResponseDirOptions,
|
||||
} from '../types';
|
||||
|
||||
// Matches a directory name + one `/` at the start of any string,
|
||||
// containing any character except / one or more times, and ending with a `/`
|
||||
// e.g. Will match `dirA/` in `dirA/dirB/file.ext`
|
||||
const directoryNameRegex = /^[^\/]+\//;
|
||||
|
||||
/**
|
||||
* Wraps a zip archive stream into a tree response reader.
|
||||
*/
|
||||
@@ -60,18 +65,26 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
this.read = true;
|
||||
}
|
||||
|
||||
private getPath(entry: Entry): string {
|
||||
return entry.path.slice(this.subPath.length);
|
||||
// Will remove the top level dir name from the path since its name is hard to predetermine.
|
||||
private stripTopDirectory(path: string): string {
|
||||
return path.replace(directoryNameRegex, '');
|
||||
}
|
||||
|
||||
// File path relative to the root extracted directory or a sub directory if subpath is set.
|
||||
private getInnerPath(path: string): string {
|
||||
return path.slice(this.subPath.length);
|
||||
}
|
||||
|
||||
private shouldBeIncluded(entry: Entry): boolean {
|
||||
const strippedPath = this.stripTopDirectory(entry.path);
|
||||
|
||||
if (this.subPath) {
|
||||
if (!entry.path.startsWith(this.subPath)) {
|
||||
if (!strippedPath.startsWith(this.subPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.filter) {
|
||||
return this.filter(this.getPath(entry));
|
||||
return this.filter(this.getInnerPath(entry.path));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -91,7 +104,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
|
||||
if (this.shouldBeIncluded(entry)) {
|
||||
files.push({
|
||||
path: this.getPath(entry),
|
||||
path: this.getInnerPath(this.stripTopDirectory(entry.path)),
|
||||
content: () => entry.buffer(),
|
||||
});
|
||||
} else {
|
||||
@@ -115,7 +128,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
.pipe(unzipper.Parse())
|
||||
.on('entry', (entry: Entry) => {
|
||||
if (entry.type === 'File' && this.shouldBeIncluded(entry)) {
|
||||
archive.append(entry, { name: this.getPath(entry) });
|
||||
archive.append(entry, { name: this.getInnerPath(entry.path) });
|
||||
} else {
|
||||
entry.autodrain();
|
||||
}
|
||||
@@ -139,7 +152,9 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
// Ignore directory entries since we handle that with the file entries
|
||||
// as a zip can have files with directories without directory entries
|
||||
if (entry.type === 'File' && this.shouldBeIncluded(entry)) {
|
||||
const entryPath = this.getPath(entry);
|
||||
const entryPath = this.getInnerPath(
|
||||
this.stripTopDirectory(entry.path),
|
||||
);
|
||||
const dirname = platformPath.dirname(entryPath);
|
||||
if (dirname) {
|
||||
await fs.mkdirp(platformPath.join(dir, dirname));
|
||||
|
||||
@@ -81,6 +81,9 @@ export type ReadTreeResponseDirOptions = {
|
||||
};
|
||||
|
||||
export type ReadTreeResponse = {
|
||||
/**
|
||||
* files() returns an array of all the files inside the tree and corresponding functions to read their content.
|
||||
*/
|
||||
files(): Promise<ReadTreeResponseFile[]>;
|
||||
archive(): Promise<NodeJS.ReadableStream>;
|
||||
|
||||
|
||||
@@ -86,13 +86,22 @@ export class Git {
|
||||
return git.commit({ fs, dir, message, author, committer });
|
||||
}
|
||||
|
||||
async clone({ url, dir }: { url: string; dir: string }): Promise<void> {
|
||||
async clone({
|
||||
url,
|
||||
dir,
|
||||
ref,
|
||||
}: {
|
||||
url: string;
|
||||
dir: string;
|
||||
ref?: string;
|
||||
}): Promise<void> {
|
||||
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
|
||||
return git.clone({
|
||||
fs,
|
||||
http,
|
||||
url,
|
||||
dir,
|
||||
ref,
|
||||
singleBranch: true,
|
||||
depth: 1,
|
||||
onProgress: this.onProgressHandler(),
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# example-backend
|
||||
|
||||
## 0.2.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [26a3a6cf0]
|
||||
- Updated dependencies [681111228]
|
||||
- Updated dependencies [664dd08c9]
|
||||
- Updated dependencies [9dd057662]
|
||||
- Updated dependencies [234e7d985]
|
||||
- Updated dependencies [d7b1d317f]
|
||||
- Updated dependencies [a91aa6bf2]
|
||||
- Updated dependencies [39b05b9ae]
|
||||
- Updated dependencies [4eaa06057]
|
||||
- @backstage/backend-common@0.5.1
|
||||
- @backstage/plugin-scaffolder-backend@0.5.2
|
||||
- @backstage/plugin-kubernetes-backend@0.2.6
|
||||
- @backstage/plugin-catalog-backend@0.5.5
|
||||
- @backstage/plugin-kafka-backend@0.2.0
|
||||
- @backstage/plugin-auth-backend@0.2.12
|
||||
- example-app@0.2.13
|
||||
- @backstage/plugin-app-backend@0.3.5
|
||||
|
||||
## 0.2.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend",
|
||||
"version": "0.2.12",
|
||||
"version": "0.2.13",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -27,24 +27,24 @@
|
||||
"migrate:create": "knex migrate:make -x ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.5.0",
|
||||
"@backstage/backend-common": "^0.5.1",
|
||||
"@backstage/catalog-model": "^0.7.0",
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/plugin-app-backend": "^0.3.4",
|
||||
"@backstage/plugin-auth-backend": "^0.2.11",
|
||||
"@backstage/plugin-catalog-backend": "^0.5.4",
|
||||
"@backstage/plugin-app-backend": "^0.3.5",
|
||||
"@backstage/plugin-auth-backend": "^0.2.12",
|
||||
"@backstage/plugin-catalog-backend": "^0.5.5",
|
||||
"@backstage/plugin-graphql-backend": "^0.1.5",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.2.5",
|
||||
"@backstage/plugin-kafka-backend": "^0.1.1",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.2.6",
|
||||
"@backstage/plugin-kafka-backend": "^0.2.0",
|
||||
"@backstage/plugin-proxy-backend": "^0.2.4",
|
||||
"@backstage/plugin-rollbar-backend": "^0.1.7",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.5.0",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.5.2",
|
||||
"@backstage/plugin-techdocs-backend": "^0.5.4",
|
||||
"@gitbeaker/node": "^28.0.2",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"azure-devops-node-api": "^10.1.1",
|
||||
"dockerode": "^3.2.1",
|
||||
"example-app": "^0.2.12",
|
||||
"example-app": "^0.2.13",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"knex": "^0.21.6",
|
||||
@@ -54,7 +54,7 @@
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.4.7",
|
||||
"@backstage/cli": "^0.5.0",
|
||||
"@types/dockerode": "^3.2.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5"
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"cross-fetch": "^3.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.4.7",
|
||||
"@backstage/cli": "^0.5.0",
|
||||
"@types/jest": "^26.0.7",
|
||||
"msw": "^0.21.2"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,6 @@ spec:
|
||||
profile:
|
||||
displayName: Backstage
|
||||
email: backstage@example.com
|
||||
picture: https://example.com/groups/backstage.jpeg
|
||||
picture: https://avatars.dicebear.com/api/identicon/backstage@example.com.svg?background=%23fff&margin=25
|
||||
parent: infrastructure
|
||||
children: [team-a, team-b]
|
||||
|
||||
@@ -3,12 +3,17 @@ kind: Group
|
||||
metadata:
|
||||
name: acme-corp
|
||||
description: The acme-corp organization
|
||||
links:
|
||||
- url: http://www.acme.com/
|
||||
title: Website
|
||||
- url: https://meta.wikimedia.org/wiki/
|
||||
title: Intranet
|
||||
spec:
|
||||
type: organization
|
||||
profile:
|
||||
displayName: ACME Corp
|
||||
email: info@example.com
|
||||
picture: https://example.com/logo.jpeg
|
||||
picture: https://avatars.dicebear.com/api/identicon/info@example.com.svg?background=%23fff&margin=25
|
||||
children: [infrastructure]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
|
||||
@@ -8,7 +8,7 @@ spec:
|
||||
profile:
|
||||
# Intentional no displayName for testing
|
||||
email: team-a@example.com
|
||||
picture: https://example.com/groups/team-a.jpeg
|
||||
picture: https://avatars.dicebear.com/api/identicon/team-a@example.com.svg?background=%23fff&margin=25
|
||||
parent: backstage
|
||||
children: []
|
||||
---
|
||||
@@ -20,7 +20,7 @@ spec:
|
||||
profile:
|
||||
# Intentional no displayName for testing
|
||||
email: breanna-davison@example.com
|
||||
picture: https://example.com/staff/breanna.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg?background=%23fff
|
||||
memberOf: [team-a]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -31,7 +31,7 @@ spec:
|
||||
profile:
|
||||
displayName: Janelle Dawe
|
||||
email: janelle-dawe@example.com
|
||||
picture: https://example.com/staff/janelle.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/janelle-dawe@example.com.svg?background=%23fff
|
||||
memberOf: [team-a]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -42,7 +42,7 @@ spec:
|
||||
profile:
|
||||
displayName: Nigel Manning
|
||||
email: nigel-manning@example.com
|
||||
picture: https://example.com/staff/nigel.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/nigel-manning@example.com.svg?background=%23fff
|
||||
memberOf: [team-a]
|
||||
---
|
||||
# This user is added as an example, to make it more easy for the "Guest"
|
||||
@@ -56,5 +56,5 @@ spec:
|
||||
profile:
|
||||
displayName: Guest User
|
||||
email: guest@example.com
|
||||
picture: https://example.com/staff/the-ceos-dog.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/guest@example.com.svg?background=%23fff
|
||||
memberOf: [team-a]
|
||||
|
||||
@@ -8,7 +8,7 @@ spec:
|
||||
profile:
|
||||
displayName: Team B
|
||||
email: team-b@example.com
|
||||
picture: https://example.com/groups/team-b.jpeg
|
||||
picture: https://avatars.dicebear.com/api/identicon/team-b@example.com.svg?background=%23fff&margin=25
|
||||
parent: backstage
|
||||
children: []
|
||||
---
|
||||
@@ -20,7 +20,7 @@ spec:
|
||||
profile:
|
||||
displayName: Amelia Park
|
||||
email: amelia-park@example.com
|
||||
picture: https://example.com/staff/amelia.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/amelia-park@example.com.svg?background=%23fff
|
||||
memberOf: [team-b]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -31,7 +31,7 @@ spec:
|
||||
profile:
|
||||
displayName: Colette Brock
|
||||
email: colette-brock@example.com
|
||||
picture: https://example.com/staff/colette.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/colette-brock@example.com.svg?background=%23fff
|
||||
memberOf: [team-b]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -42,7 +42,7 @@ spec:
|
||||
profile:
|
||||
displayName: Jenny Doe
|
||||
email: jenny-doe@example.com
|
||||
picture: https://example.com/staff/jenny.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/jenny-doe@example.com.svg?background=%23fff
|
||||
memberOf: [team-b]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -53,7 +53,7 @@ spec:
|
||||
profile:
|
||||
displayName: Jonathon Page
|
||||
email: jonathon-page@example.com
|
||||
picture: https://example.com/staff/jonathon.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/jonathon-page@example.com.svg?background=%23fff
|
||||
memberOf: [team-b]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -64,5 +64,5 @@ spec:
|
||||
profile:
|
||||
displayName: Justine Barrow
|
||||
email: justine-barrow@example.com
|
||||
picture: https://example.com/staff/justine.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/justine-barrow@example.com.svg?background=%23fff
|
||||
memberOf: [team-b]
|
||||
|
||||
@@ -8,7 +8,7 @@ spec:
|
||||
profile:
|
||||
displayName: Team C
|
||||
email: team-c@example.com
|
||||
picture: https://example.com/groups/team-c.jpeg
|
||||
picture: https://avatars.dicebear.com/api/identicon/team-c@example.com.svg?background=%23fff&margin=25
|
||||
parent: boxoffice
|
||||
children: []
|
||||
---
|
||||
@@ -20,7 +20,7 @@ spec:
|
||||
profile:
|
||||
displayName: Calum Leavy
|
||||
email: calum-leavy@example.com
|
||||
picture: https://example.com/staff/calum.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/calum-leavy@example.com.svg?background=%23fff
|
||||
memberOf: [team-c]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -31,7 +31,7 @@ spec:
|
||||
profile:
|
||||
displayName: Frank Tiernan
|
||||
email: frank-tiernan@example.com
|
||||
picture: https://example.com/staff/frank.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/frank-tiernan@example.com.svg?background=%23fff
|
||||
memberOf: [team-c]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -42,7 +42,7 @@ spec:
|
||||
profile:
|
||||
displayName: Peadar MacMahon
|
||||
email: peadar-macmahon@example.com
|
||||
picture: https://example.com/staff/peadar.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/peadar-macmahon@example.com.svg?background=%23fff
|
||||
memberOf: [team-c]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -53,7 +53,7 @@ spec:
|
||||
profile:
|
||||
displayName: Sarah Gilroy
|
||||
email: sarah-gilroy@example.com
|
||||
picture: https://example.com/staff/sarah.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/sarah-gilroy@example.com.svg?background=%23fff
|
||||
memberOf: [team-c]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -64,5 +64,5 @@ spec:
|
||||
profile:
|
||||
displayName: Tara MacGovern
|
||||
email: tara-macgovern@example.com
|
||||
picture: https://example.com/staff/tara.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/tara-macgovern@example.com.svg?background=%23fff
|
||||
memberOf: [team-c]
|
||||
|
||||
@@ -8,7 +8,7 @@ spec:
|
||||
profile:
|
||||
displayName: Team D
|
||||
email: team-d@example.com
|
||||
picture: https://example.com/groups/team-d.jpeg
|
||||
picture: https://avatars.dicebear.com/api/identicon/team-d@example.com.svg?background=%23fff&margin=25
|
||||
parent: boxoffice
|
||||
children: []
|
||||
---
|
||||
@@ -20,7 +20,7 @@ spec:
|
||||
profile:
|
||||
displayName: Eva MacDowell
|
||||
email: eva-macdowell@example.com
|
||||
picture: https://example.com/staff/eva.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/eva-macdowell@example.com.svg?background=%23fff
|
||||
memberOf: [team-d]
|
||||
---
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
@@ -31,5 +31,5 @@ spec:
|
||||
profile:
|
||||
displayName: Lucy Sheehan
|
||||
email: lucy-sheehan@example.com
|
||||
picture: https://example.com/staff/lucy.jpeg
|
||||
picture: https://avatars.dicebear.com/api/avataaars/lucy-sheehan@example.com.svg?background=%23fff
|
||||
memberOf: [team-d]
|
||||
|
||||
@@ -6,6 +6,13 @@ metadata:
|
||||
tags:
|
||||
- store
|
||||
- rest
|
||||
links:
|
||||
- url: https://github.com/swagger-api/swagger-petstore
|
||||
title: GitHub Repo
|
||||
icon: github
|
||||
- url: https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml
|
||||
title: API Spec
|
||||
icon: code
|
||||
spec:
|
||||
type: openapi
|
||||
lifecycle: experimental
|
||||
|
||||
@@ -5,6 +5,10 @@ metadata:
|
||||
description: The Smartylighting Streetlights API allows you to remotely manage the city lights.
|
||||
tags:
|
||||
- mqtt
|
||||
links:
|
||||
- url: https://github.com/asyncapi/asyncapi/blob/master/examples/1.2.0/streetlights.yml
|
||||
title: Source Code
|
||||
icon: code
|
||||
spec:
|
||||
type: asyncapi
|
||||
lifecycle: production
|
||||
|
||||
@@ -3,6 +3,10 @@ kind: API
|
||||
metadata:
|
||||
name: starwars-graphql
|
||||
description: SWAPI GraphQL Schema
|
||||
links:
|
||||
- url: https://github.com/graphql/swapi-graphql
|
||||
title: GitHub Repo
|
||||
icon: github
|
||||
spec:
|
||||
type: graphql
|
||||
lifecycle: production
|
||||
|
||||
@@ -6,6 +6,12 @@ metadata:
|
||||
tags:
|
||||
- java
|
||||
- data
|
||||
links:
|
||||
- url: https://example.com/apm/artists-lookup
|
||||
title: APM
|
||||
icon: dashboard
|
||||
- url: https://example.com/logs/artists-lookup
|
||||
title: Logs
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
|
||||
@@ -3,6 +3,10 @@ kind: Component
|
||||
metadata:
|
||||
name: petstore
|
||||
description: Petstore
|
||||
links:
|
||||
- url: https://github.com/swagger-api/swagger-petstore
|
||||
title: GitHub Repo
|
||||
icon: github
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
|
||||
@@ -3,5 +3,11 @@ kind: Domain
|
||||
metadata:
|
||||
name: artists
|
||||
description: Everything related to artists
|
||||
links:
|
||||
- url: http://example.com/domain/artists/
|
||||
title: Domain Readme
|
||||
- url: http://example.com/domains/artists/dashboard
|
||||
title: Domain Metrics Dashboard
|
||||
icon: dashboard
|
||||
spec:
|
||||
owner: team-a
|
||||
|
||||
@@ -32,13 +32,14 @@
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"@types/yup": "^0.29.8",
|
||||
"ajv": "^7.0.3",
|
||||
"json-schema": "^0.2.5",
|
||||
"lodash": "^4.17.15",
|
||||
"uuid": "^8.0.0",
|
||||
"yup": "^0.29.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.4.7",
|
||||
"@backstage/cli": "^0.5.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/lodash": "^4.14.151",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Entity } from './entity';
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
@@ -15,8 +14,8 @@ import { Entity } from './entity';
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, EntityPolicy } from './entity';
|
||||
import { EntityPolicies } from './EntityPolicies';
|
||||
import { EntityPolicy } from './types';
|
||||
|
||||
describe('EntityPolicies', () => {
|
||||
const p1: jest.Mocked<EntityPolicy> = { enforce: jest.fn() };
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from './entity';
|
||||
import { EntityPolicy } from './types';
|
||||
import { Entity, EntityPolicy } from './entity';
|
||||
|
||||
// Helper that requires that all of a set of policies can be successfully
|
||||
// applied
|
||||
|
||||
@@ -125,6 +125,11 @@ export type EntityMeta = JsonObject & {
|
||||
* various ways.
|
||||
*/
|
||||
tags?: string[];
|
||||
|
||||
/**
|
||||
* A list of external hyperlinks related to the entity.
|
||||
*/
|
||||
links?: EntityLink[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -161,3 +166,23 @@ export type EntityRelationSpec = {
|
||||
*/
|
||||
target: EntityName;
|
||||
};
|
||||
|
||||
/**
|
||||
* A link to external information that is related to the entity.
|
||||
*/
|
||||
export type EntityLink = {
|
||||
/**
|
||||
* The url to the external site, document, etc.
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* An optional descriptive title for the link.
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* An optional semantic key that represents a visual icon.
|
||||
*/
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ export {
|
||||
} from './constants';
|
||||
export type {
|
||||
Entity,
|
||||
EntityLink,
|
||||
EntityMeta,
|
||||
EntityRelation,
|
||||
EntityRelationSpec,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import lodash from 'lodash';
|
||||
import { EntityPolicy } from '../../types';
|
||||
import { EntityPolicy } from './types';
|
||||
import { ENTITY_DEFAULT_NAMESPACE } from '../constants';
|
||||
import { Entity } from '../Entity';
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ describe('FieldFormatEntityPolicy', () => {
|
||||
tags:
|
||||
- java
|
||||
- data-service
|
||||
links:
|
||||
- url: https://example.org
|
||||
title: Website
|
||||
icon: website
|
||||
spec:
|
||||
custom: stuff
|
||||
`);
|
||||
@@ -110,4 +114,110 @@ describe('FieldFormatEntityPolicy', () => {
|
||||
data.metadata.tags.push('Hello World');
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/tags.*"Hello World"/i);
|
||||
});
|
||||
|
||||
it('accepts missing links', async () => {
|
||||
delete data.metadata.links;
|
||||
await expect(policy.enforce(data)).resolves.toBe(data);
|
||||
});
|
||||
|
||||
it('accepts empty links array', async () => {
|
||||
data.metadata.links = [];
|
||||
await expect(policy.enforce(data)).resolves.toBe(data);
|
||||
});
|
||||
|
||||
it('accepts multiple links', async () => {
|
||||
data.metadata.links = [{ url: 'http://foo' }, { url: 'https://bar' }];
|
||||
await expect(policy.enforce(data)).resolves.toBe(data);
|
||||
});
|
||||
|
||||
it('rejects missing link url value', async () => {
|
||||
data.metadata.links = [{}];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/links.0.url/i);
|
||||
});
|
||||
|
||||
it('rejects a single bad missing link url value', async () => {
|
||||
data.metadata.links = [{ url: 'http://good' }, { url: '' }];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(
|
||||
/links.1.url.*valid url/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects empty link url value', async () => {
|
||||
data.metadata.links = [{ url: '' }];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/links.0.url.*/i);
|
||||
});
|
||||
|
||||
it('rejects bad link url value', async () => {
|
||||
data.metadata.links = [{ url: 'invalid' }];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(
|
||||
/links.0.url.*"invalid"/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts missing link title', async () => {
|
||||
data.metadata.links = [{ url: 'http://foo', icon: 'dashboard' }];
|
||||
await expect(policy.enforce(data)).resolves.toBe(data);
|
||||
});
|
||||
|
||||
it('rejects empty link title', async () => {
|
||||
data.metadata.links = [{ url: 'http://foo', title: '' }];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/links.0.title.*""/i);
|
||||
});
|
||||
|
||||
it('rejects bad link title', async () => {
|
||||
data.metadata.links = [{ url: 'http://foo', title: 123 }];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/links.0.title.*"123"/i);
|
||||
});
|
||||
|
||||
it.each([[123], [{}], [[]]])(
|
||||
'rejects bad link title %s',
|
||||
async (title: unknown) => {
|
||||
data.metadata.links = [{ url: 'http://foo', title }];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/links.0.title.*/i);
|
||||
},
|
||||
);
|
||||
|
||||
it('rejects a single bad link title', async () => {
|
||||
data.metadata.links = [
|
||||
{ url: 'http://foo', title: 'good' },
|
||||
{ url: 'http://foo', title: '' },
|
||||
];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/links.1.title.*""/i);
|
||||
});
|
||||
|
||||
it('accepts missing link icon', async () => {
|
||||
data.metadata.links = [{ url: 'http://foo', title: 'foo' }];
|
||||
await expect(policy.enforce(data)).resolves.toBe(data);
|
||||
});
|
||||
|
||||
it('rejects empty link icon', async () => {
|
||||
data.metadata.links = [{ url: 'http://foo', icon: '' }];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/links.0.icon.*""/i);
|
||||
});
|
||||
|
||||
it.each([['dashboard'], ['admin-dashboard'], ['foo_dashboard']])(
|
||||
'accepts valid link icon',
|
||||
async icon => {
|
||||
data.metadata.links = [{ url: 'http://foo', icon }];
|
||||
await expect(policy.enforce(data)).resolves.toBe(data);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([[123], [{}], [[]], ['abc xyz']])(
|
||||
'rejects bad link icon value %s',
|
||||
async (icon: unknown) => {
|
||||
data.metadata.links = [{ url: 'http://foo', icon }];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/links.0.icon.*/i);
|
||||
},
|
||||
);
|
||||
|
||||
it('rejects a single bad link icon value', async () => {
|
||||
data.metadata.links = [
|
||||
{ url: 'http://foo', icon: 'good' },
|
||||
{ url: 'http://foo', icon: 'not good' },
|
||||
];
|
||||
await expect(policy.enforce(data)).rejects.toThrow(
|
||||
/links.1.icon.*"not good"/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityPolicy } from '../../types';
|
||||
import { EntityPolicy } from './types';
|
||||
import {
|
||||
CommonValidatorFunctions,
|
||||
KubernetesValidatorFunctions,
|
||||
@@ -83,6 +83,12 @@ export class FieldFormatEntityPolicy implements EntityPolicy {
|
||||
expectation =
|
||||
'a string that is a sequence of [a-zA-Z][a-z0-9A-Z], at most 63 characters in total';
|
||||
break;
|
||||
case 'isValidUrl':
|
||||
expectation = 'a string that is a valid url';
|
||||
break;
|
||||
case 'isValidString':
|
||||
expectation = 'a non empty string';
|
||||
break;
|
||||
default:
|
||||
expectation = undefined;
|
||||
break;
|
||||
@@ -134,6 +140,23 @@ export class FieldFormatEntityPolicy implements EntityPolicy {
|
||||
require(`tags.${i}`, tags[i], this.validators.isValidTag);
|
||||
}
|
||||
|
||||
const links = entity.metadata.links ?? [];
|
||||
|
||||
for (let i = 0; i < links.length; ++i) {
|
||||
require(`links.${i}.url`, links[i]
|
||||
?.url, CommonValidatorFunctions.isValidUrl);
|
||||
optional(
|
||||
`links.${i}.title`,
|
||||
links[i]?.title,
|
||||
CommonValidatorFunctions.isValidString,
|
||||
);
|
||||
optional(
|
||||
`links.${i}.icon`,
|
||||
links[i]?.icon,
|
||||
KubernetesValidatorFunctions.isValidObjectName,
|
||||
);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityPolicy } from '../../types';
|
||||
import { EntityPolicy } from './types';
|
||||
import { Entity } from '../Entity';
|
||||
|
||||
const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec'];
|
||||
|
||||
@@ -39,6 +39,10 @@ describe('SchemaValidEntityPolicy', () => {
|
||||
tags:
|
||||
- java
|
||||
- data
|
||||
links:
|
||||
- url: https://example.com
|
||||
title: Website
|
||||
icon: website
|
||||
spec:
|
||||
custom: stuff
|
||||
`);
|
||||
@@ -198,6 +202,24 @@ describe('SchemaValidEntityPolicy', () => {
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/tags/);
|
||||
});
|
||||
|
||||
it('accepts missing links', async () => {
|
||||
delete data.metadata.links;
|
||||
await expect(policy.enforce(data)).resolves.toBe(data);
|
||||
});
|
||||
|
||||
it('accepts empty links array', async () => {
|
||||
data.metadata.links = [];
|
||||
await expect(policy.enforce(data)).resolves.toBe(data);
|
||||
});
|
||||
|
||||
it.each([['invalid type'], [123], [{}], [{ url: 'https://foo' }]])(
|
||||
'rejects bad links type %s',
|
||||
async (val: unknown) => {
|
||||
data.metadata.links = val;
|
||||
await expect(policy.enforce(data)).rejects.toThrow(/links/);
|
||||
},
|
||||
);
|
||||
|
||||
//
|
||||
// spec
|
||||
//
|
||||
|
||||
@@ -14,30 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
import { EntityPolicy } from '../../types';
|
||||
import Ajv, { ValidateFunction } from 'ajv';
|
||||
import entitySchema from '../../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../../schema/shared/common.schema.json';
|
||||
import { Entity } from '../Entity';
|
||||
|
||||
const DEFAULT_ENTITY_SCHEMA = yup
|
||||
.object({
|
||||
apiVersion: yup.string().required(),
|
||||
kind: yup.string().required(),
|
||||
metadata: yup
|
||||
.object({
|
||||
uid: yup.string().notRequired().min(1),
|
||||
etag: yup.string().notRequired().min(1),
|
||||
generation: yup.number().notRequired().integer().min(1),
|
||||
name: yup.string().required(),
|
||||
namespace: yup.string().notRequired(),
|
||||
description: yup.string().notRequired(),
|
||||
labels: yup.object<Record<string, string>>().notRequired(),
|
||||
annotations: yup.object<Record<string, string>>().notRequired(),
|
||||
tags: yup.array<string>().notRequired(),
|
||||
})
|
||||
.required(),
|
||||
spec: yup.object({}).notRequired(),
|
||||
})
|
||||
.required();
|
||||
import { EntityPolicy } from './types';
|
||||
|
||||
/**
|
||||
* Ensures that the entity spec is valid according to a schema.
|
||||
@@ -47,17 +29,28 @@ const DEFAULT_ENTITY_SCHEMA = yup
|
||||
* typescript type.
|
||||
*/
|
||||
export class SchemaValidEntityPolicy implements EntityPolicy {
|
||||
private readonly schema: yup.Schema<Entity>;
|
||||
|
||||
constructor(schema: yup.Schema<Entity> = DEFAULT_ENTITY_SCHEMA) {
|
||||
this.schema = schema;
|
||||
}
|
||||
private validate: ValidateFunction<Entity> | undefined;
|
||||
|
||||
async enforce(entity: Entity): Promise<Entity> {
|
||||
try {
|
||||
return await this.schema.validate(entity, { strict: true });
|
||||
} catch (e) {
|
||||
throw new Error(`Malformed envelope, ${e}`);
|
||||
if (!this.validate) {
|
||||
const ajv = new Ajv({ allowUnionTypes: true });
|
||||
this.validate = ajv
|
||||
.addSchema([commonSchema, entityMetaSchema], undefined, undefined, true)
|
||||
.compile<Entity>(entitySchema);
|
||||
}
|
||||
|
||||
const result = this.validate(entity);
|
||||
if (result === true) {
|
||||
return entity;
|
||||
}
|
||||
|
||||
const [error] = this.validate.errors || [];
|
||||
if (!error) {
|
||||
throw new Error(`Malformed envelope, Unknown error`);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Malformed envelope, ${error.dataPath || '<root>'} ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,3 +18,4 @@ export { DefaultNamespaceEntityPolicy } from './DefaultNamespaceEntityPolicy';
|
||||
export { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy';
|
||||
export { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy';
|
||||
export { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy';
|
||||
export type { EntityPolicy } from './types';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 type { Entity } from '../Entity';
|
||||
|
||||
/**
|
||||
* A policy for validation or mutation to be applied to entities as they are
|
||||
* entering the system.
|
||||
*/
|
||||
export type EntityPolicy = {
|
||||
/**
|
||||
* Applies validation or mutation on an entity.
|
||||
*
|
||||
* @param entity The entity, as validated/mutated so far in the policy tree
|
||||
* @returns The incoming entity, or a mutated version of the same, or
|
||||
* undefined if this processor could not handle the entity
|
||||
* @throws An error if the entity should be rejected
|
||||
*/
|
||||
enforce(entity: Entity): Promise<Entity | undefined>;
|
||||
};
|
||||
@@ -18,5 +18,5 @@ export * from './entity';
|
||||
export { EntityPolicies } from './EntityPolicies';
|
||||
export * from './kinds';
|
||||
export * from './location';
|
||||
export type { EntityName, EntityPolicy, EntityRef, JSONSchema } from './types';
|
||||
export type { EntityName, EntityRef, JSONSchema } from './types';
|
||||
export * from './validation';
|
||||
|
||||
@@ -14,27 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import { schemaValidator } from './util';
|
||||
import schema from '../schema/kinds/API.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'API' as const;
|
||||
|
||||
const schema = yup.object<Partial<ApiEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
lifecycle: yup.string().required().min(1),
|
||||
owner: yup.string().required().min(1),
|
||||
definition: yup.string().required().min(1),
|
||||
system: yup.string().notRequired().min(1),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface ApiEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -47,8 +36,9 @@ export interface ApiEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export const apiEntityV1alpha1Validator = schemaValidator(
|
||||
export const apiEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,29 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import { schemaValidator } from './util';
|
||||
import schema from '../schema/kinds/Component.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Component' as const;
|
||||
|
||||
const schema = yup.object<Partial<ComponentEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
lifecycle: yup.string().required().min(1),
|
||||
owner: yup.string().required().min(1),
|
||||
subcomponentOf: yup.string().notRequired().min(1),
|
||||
providesApis: yup.array(yup.string().required()).notRequired(),
|
||||
consumesApis: yup.array(yup.string().required()).notRequired(),
|
||||
system: yup.string().notRequired().min(1),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface ComponentEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -51,8 +38,9 @@ export interface ComponentEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export const componentEntityV1alpha1Validator = schemaValidator(
|
||||
export const componentEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,23 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import { schemaValidator } from './util';
|
||||
import schema from '../schema/kinds/Domain.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Domain' as const;
|
||||
|
||||
const schema = yup.object<Partial<DomainEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
owner: yup.string().required().min(1),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface DomainEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -39,8 +32,9 @@ export interface DomainEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export const domainEntityV1alpha1Validator = schemaValidator(
|
||||
export const domainEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,40 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import { schemaValidator } from './util';
|
||||
import schema from '../schema/kinds/Group.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Group' as const;
|
||||
|
||||
const schema = yup.object<Partial<GroupEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
profile: yup
|
||||
.object({
|
||||
displayName: yup.string().min(1).notRequired(),
|
||||
email: yup.string().min(1).notRequired(),
|
||||
picture: yup.string().min(1).notRequired(),
|
||||
})
|
||||
.notRequired(),
|
||||
parent: yup.string().notRequired().min(1),
|
||||
// Use these manual tests because yup .required() requires at least
|
||||
// one element and there is no simple workaround -_-
|
||||
// the cast is there to convince typescript that the array itself is
|
||||
// required without using .required()
|
||||
children: yup.array(yup.string().required()).test({
|
||||
name: 'isDefined',
|
||||
message: 'children must be defined',
|
||||
test: v => Boolean(v),
|
||||
}) as yup.ArraySchema<string, object>,
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface GroupEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -63,8 +39,9 @@ export interface GroupEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export const groupEntityV1alpha1Validator = schemaValidator(
|
||||
export const groupEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,25 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import { schemaValidator } from './util';
|
||||
import schema from '../schema/kinds/Location.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Location' as const;
|
||||
|
||||
const schema = yup.object<Partial<LocationEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().notRequired().min(1),
|
||||
target: yup.string().notRequired().min(1),
|
||||
targets: yup.array(yup.string().required()).notRequired(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface LocationEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -43,8 +34,9 @@ export interface LocationEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export const locationEntityV1alpha1Validator = schemaValidator(
|
||||
export const locationEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,25 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import { schemaValidator } from './util';
|
||||
import schema from '../schema/kinds/Resource.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Resource' as const;
|
||||
|
||||
const schema = yup.object<Partial<ResourceEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
owner: yup.string().required().min(1),
|
||||
system: yup.string().notRequired().min(1),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface ResourceEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -43,8 +34,9 @@ export interface ResourceEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export const resourceEntityV1alpha1Validator = schemaValidator(
|
||||
export const resourceEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,24 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import { schemaValidator } from './util';
|
||||
import schema from '../schema/kinds/System.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'System' as const;
|
||||
|
||||
const schema = yup.object<Partial<SystemEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
owner: yup.string().required().min(1),
|
||||
domain: yup.string().notRequired().min(1),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface SystemEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -41,8 +33,9 @@ export interface SystemEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export const systemEntityV1alpha1Validator = schemaValidator(
|
||||
export const systemEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,27 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import schema from '../schema/kinds/Template.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import type { JSONSchema } from '../types';
|
||||
import { schemaValidator } from './util';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'Template' as const;
|
||||
|
||||
const schema = yup.object<Partial<TemplateEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
type: yup.string().required().min(1),
|
||||
path: yup.string(),
|
||||
schema: yup.object().required(),
|
||||
templater: yup.string().required(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface TemplateEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -46,8 +36,9 @@ export interface TemplateEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export const templateEntityV1alpha1Validator = schemaValidator(
|
||||
export const templateEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,38 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
import type { Entity } from '../entity/Entity';
|
||||
import { schemaValidator } from './util';
|
||||
import schema from '../schema/kinds/User.v1alpha1.schema.json';
|
||||
import entitySchema from '../schema/Entity.schema.json';
|
||||
import entityMetaSchema from '../schema/EntityMeta.schema.json';
|
||||
import commonSchema from '../schema/shared/common.schema.json';
|
||||
import { ajvCompiledJsonSchemaValidator } from './util';
|
||||
|
||||
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
|
||||
const KIND = 'User' as const;
|
||||
|
||||
const schema = yup.object<Partial<UserEntityV1alpha1>>({
|
||||
apiVersion: yup.string().required().oneOf(API_VERSION),
|
||||
kind: yup.string().required().equals([KIND]),
|
||||
spec: yup
|
||||
.object({
|
||||
profile: yup
|
||||
.object({
|
||||
displayName: yup.string().min(1).notRequired(),
|
||||
email: yup.string().min(1).notRequired(),
|
||||
picture: yup.string().min(1).notRequired(),
|
||||
})
|
||||
.notRequired(),
|
||||
// Use this manual test because yup .required() requires at least one
|
||||
// element and there is no simple workaround -_-
|
||||
// the cast is there to convince typescript that the array itself is
|
||||
// required without using .required()
|
||||
memberOf: yup.array(yup.string().required()).test({
|
||||
name: 'isDefined',
|
||||
message: 'memberOf must be defined',
|
||||
test: v => Boolean(v),
|
||||
}) as yup.ArraySchema<string, object>,
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export interface UserEntityV1alpha1 extends Entity {
|
||||
apiVersion: typeof API_VERSION[number];
|
||||
kind: typeof KIND;
|
||||
@@ -59,8 +37,9 @@ export interface UserEntityV1alpha1 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
export const userEntityV1alpha1Validator = schemaValidator(
|
||||
export const userEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
|
||||
KIND,
|
||||
API_VERSION,
|
||||
schema,
|
||||
[commonSchema, entityMetaSchema, entitySchema],
|
||||
);
|
||||
|
||||
@@ -14,9 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Ajv, { AnySchema } from 'ajv';
|
||||
import * as yup from 'yup';
|
||||
import { KindValidator } from './types';
|
||||
|
||||
/**
|
||||
* @deprecated We no longer use yup for the catalog model. This utility method will be removed.
|
||||
*/
|
||||
export function schemaValidator(
|
||||
kind: string,
|
||||
apiVersion: readonly string[],
|
||||
@@ -24,10 +28,7 @@ export function schemaValidator(
|
||||
): KindValidator {
|
||||
return {
|
||||
async check(envelope) {
|
||||
if (
|
||||
kind !== envelope.kind ||
|
||||
!apiVersion.includes(envelope.apiVersion as any)
|
||||
) {
|
||||
if (kind !== envelope.kind || !apiVersion.includes(envelope.apiVersion)) {
|
||||
return false;
|
||||
}
|
||||
await schema.validate(envelope, { strict: true });
|
||||
@@ -35,3 +36,38 @@ export function schemaValidator(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function ajvCompiledJsonSchemaValidator(
|
||||
kind: string,
|
||||
apiVersion: readonly string[],
|
||||
schema: AnySchema,
|
||||
extraSchemas?: AnySchema[],
|
||||
): KindValidator {
|
||||
const ajv = new Ajv({ allowUnionTypes: true });
|
||||
if (extraSchemas) {
|
||||
ajv.addSchema(extraSchemas, undefined, undefined, true);
|
||||
}
|
||||
const validate = ajv.compile(schema);
|
||||
|
||||
return {
|
||||
async check(envelope) {
|
||||
if (kind !== envelope.kind || !apiVersion.includes(envelope.apiVersion)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = validate(envelope);
|
||||
if (result === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [error] = validate.errors || [];
|
||||
if (!error) {
|
||||
throw new TypeError(`Malformed ${kind}, Unknown error`);
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
`Malformed ${kind}, ${error.dataPath || '<root>'} ${error.message}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "Entity",
|
||||
"description": "The format envelope that's common to all versions/kinds of entity.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Component",
|
||||
"metadata": {
|
||||
"name": "LoremService",
|
||||
"description": "Creates Lorems like a pro.",
|
||||
"labels": {
|
||||
"product_name": "Random value Generator"
|
||||
},
|
||||
"annotations": {
|
||||
"docs": "https://github.com/..../tree/develop/doc"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"type": "service",
|
||||
"lifecycle": "production",
|
||||
"owner": "tools"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "object",
|
||||
"required": ["apiVersion", "kind", "metadata"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"type": "string",
|
||||
"description": "The version of specification format for this particular entity that this is written against.",
|
||||
"minLength": 1,
|
||||
"examples": ["backstage.io/v1alpha1", "my-company.net/v1", "1.0"]
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "The high level entity type being described.",
|
||||
"minLength": 1,
|
||||
"examples": [
|
||||
"API",
|
||||
"Component",
|
||||
"Domain",
|
||||
"Group",
|
||||
"Location",
|
||||
"Resource",
|
||||
"System",
|
||||
"Template",
|
||||
"User"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "EntityMeta"
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"description": "The specification data describing the entity itself."
|
||||
},
|
||||
"relations": {
|
||||
"type": "array",
|
||||
"description": "The relations that this entity has with other entities.",
|
||||
"items": {
|
||||
"$ref": "common#relation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "EntityMeta",
|
||||
"description": "Metadata fields common to all versions/kinds of entity.",
|
||||
"examples": [
|
||||
{
|
||||
"uid": "e01199ab-08cc-44c2-8e19-5c29ded82521",
|
||||
"etag": "lsndfkjsndfkjnsdfkjnsd==",
|
||||
"generation": 13,
|
||||
"name": "my-component-yay",
|
||||
"namespace": "the-namespace",
|
||||
"labels": {
|
||||
"backstage.io/custom": "ValueStuff"
|
||||
},
|
||||
"annotations": {
|
||||
"example.com/bindings": "are-secret"
|
||||
},
|
||||
"tags": ["java", "data"]
|
||||
}
|
||||
],
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"description": "A globally unique ID for the entity. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, but the server is free to reject requests that do so in such a way that it breaks semantics.",
|
||||
"examples": ["e01199ab-08cc-44c2-8e19-5c29ded82521"],
|
||||
"minLength": 1
|
||||
},
|
||||
"etag": {
|
||||
"type": "string",
|
||||
"description": "An opaque string that changes for each update operation to any part of the entity, including metadata. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, and the server will then reject the operation if it does not match the current stored value.",
|
||||
"examples": ["lsndfkjsndfkjnsdfkjnsd=="],
|
||||
"minLength": 1
|
||||
},
|
||||
"generation": {
|
||||
"type": "integer",
|
||||
"description": "A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations.",
|
||||
"examples": [1],
|
||||
"minimum": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the entity. Must be unique within the catalog at any given point in time, for any given namespace + kind pair.",
|
||||
"examples": ["metadata-proxy"],
|
||||
"minLength": 1
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "The namespace that the entity belongs to.",
|
||||
"default": "default",
|
||||
"examples": ["default", "admin"],
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A short (typically relatively few words, on one line) description of the entity."
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"description": "Key/value pairs of identifying information attached to the entity.",
|
||||
"additionalProperties": true,
|
||||
"patternProperties": {
|
||||
"^.+$": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"description": "Key/value pairs of non-identifying auxiliary information attached to the entity.",
|
||||
"additionalProperties": true,
|
||||
"patternProperties": {
|
||||
"^.+$": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"description": "A list of single-valued strings, to for example classify catalog entities in various ways.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"type": "array",
|
||||
"description": "A list of external hyperlinks related to the entity. Links can provide additional contextual information that may be located outside of Backstage itself. For example, an admin dashboard or external CMS page.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["url"],
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "A url in a standard uri format.",
|
||||
"examples": ["https://admin.example-org.com"],
|
||||
"minLength": 1
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A user friendly display name for the link.",
|
||||
"examples": ["Admin Dashboard"],
|
||||
"minLength": 1
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "A key representing a visual icon to be displayed in the UI.",
|
||||
"examples": ["dashboard"],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "ApiV1alpha1",
|
||||
"description": "An API describes an interface that can be exposed by a component. The API can be defined in different formats, like OpenAPI, AsyncAPI, GraphQL, gRPC, or other formats.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "API",
|
||||
"metadata": {
|
||||
"name": "artist-api",
|
||||
"description": "Retrieve artist details",
|
||||
"labels": {
|
||||
"product_name": "Random value Generator"
|
||||
},
|
||||
"annotations": {
|
||||
"docs": "https://github.com/..../tree/develop/doc"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"type": "openapi",
|
||||
"lifecycle": "production",
|
||||
"owner": "artist-relations-team",
|
||||
"system": "artist-engagement-portal",
|
||||
"definition": "openapi: \"3.0.0\"\ninfo:..."
|
||||
}
|
||||
}
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "Entity"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["spec"],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["API"]
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"required": ["type", "lifecycle", "owner", "definition"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The type of the API definition.",
|
||||
"examples": ["openapi", "asyncapi", "graphql", "grpc"],
|
||||
"minLength": 1
|
||||
},
|
||||
"lifecycle": {
|
||||
"type": "string",
|
||||
"description": "The lifecycle state of the API.",
|
||||
"examples": ["experimental", "production", "deprecated"],
|
||||
"minLength": 1
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "An entity reference to the owner of the API.",
|
||||
"examples": ["artist-relations-team", "user:john.johnson"],
|
||||
"minLength": 1
|
||||
},
|
||||
"system": {
|
||||
"type": "string",
|
||||
"description": "An entity reference to the system that the API belongs to.",
|
||||
"minLength": 1
|
||||
},
|
||||
"definition": {
|
||||
"type": "string",
|
||||
"description": "The definition of the API, based on the format defined by the type.",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "ComponentV1alpha1",
|
||||
"description": "A Component describes a software component. It is typically intimately linked to the source code that constitutes the component, and should be what a developer may regard a \"unit of software\", usually with a distinct deployable or linkable artifact.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Component",
|
||||
"metadata": {
|
||||
"name": "LoremService",
|
||||
"description": "Creates Lorems like a pro.",
|
||||
"labels": {
|
||||
"product_name": "Random value Generator"
|
||||
},
|
||||
"annotations": {
|
||||
"docs": "https://github.com/..../tree/develop/doc"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"type": "service",
|
||||
"lifecycle": "production",
|
||||
"owner": "tools"
|
||||
}
|
||||
}
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "Entity"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["spec"],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["Component"]
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"required": ["type", "lifecycle", "owner"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The type of component.",
|
||||
"examples": ["service", "website", "library"],
|
||||
"minLength": 1
|
||||
},
|
||||
"lifecycle": {
|
||||
"type": "string",
|
||||
"description": "The lifecycle state of the component.",
|
||||
"examples": ["experimental", "production", "deprecated"],
|
||||
"minLength": 1
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "An entity reference to the owner of the component.",
|
||||
"examples": ["artist-relations-team", "user:john.johnson"],
|
||||
"minLength": 1
|
||||
},
|
||||
"system": {
|
||||
"type": "string",
|
||||
"description": "An entity reference to the system that the component belongs to.",
|
||||
"minLength": 1
|
||||
},
|
||||
"subcomponentOf": {
|
||||
"type": "string",
|
||||
"description": "An entity reference to another component of which the component is a part.",
|
||||
"minLength": 1
|
||||
},
|
||||
"providesApis": {
|
||||
"type": "array",
|
||||
"description": "An array of entity references to the APIs that are provided by the component.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"consumesApis": {
|
||||
"type": "array",
|
||||
"description": "An array of entity references to the APIs that are consumed by the component.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "DomainV1alpha1",
|
||||
"description": "A Domain groups a collection of systems that share terminology, domain models, business purpose, or documentation, i.e. form a bounded context.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Domain",
|
||||
"metadata": {
|
||||
"name": "artists",
|
||||
"description": "Everything about artists"
|
||||
},
|
||||
"spec": {
|
||||
"owner": "artist-relations-team"
|
||||
}
|
||||
}
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "Entity"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["spec"],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["Domain"]
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"required": ["owner"],
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "An entity reference to the owner of the component.",
|
||||
"examples": ["artist-relations-team", "user:john.johnson"],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "GroupV1alpha1",
|
||||
"description": "A group describes an organizational entity, such as for example a team, a business unit, or a loose collection of people in an interest group. Members of these groups are modeled in the catalog as kind User.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Group",
|
||||
"metadata": {
|
||||
"name": "infrastructure",
|
||||
"description": "The infra business unit"
|
||||
},
|
||||
"spec": {
|
||||
"type": "business-unit",
|
||||
"profile": {
|
||||
"displayName": "Infrastructure",
|
||||
"email": "infrastructure@example.com",
|
||||
"picture": "https://example.com/groups/bu-infrastructure.jpeg"
|
||||
},
|
||||
"parent": "ops",
|
||||
"children": ["backstage", "other"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "Entity"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["spec"],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["Group"]
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"required": ["type", "children"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The type of group. There is currently no enforced set of values for this field, so it is left up to the adopting organization to choose a nomenclature that matches their org hierarchy.",
|
||||
"examples": ["team", "business-unit", "product-area", "root"],
|
||||
"minLength": 1
|
||||
},
|
||||
"profile": {
|
||||
"type": "object",
|
||||
"description": "Optional profile information about the group, mainly for display purposes. All fields of this structure are also optional. The email would be a group email of some form, that the group may wish to be used for contacting them. The picture is expected to be a URL pointing to an image that's representative of the group, and that a browser could fetch and render on a group page or similar.",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "A simple display name to present to users.",
|
||||
"examples": ["Infrastructure"],
|
||||
"minLength": 1
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "An email where this entity can be reached.",
|
||||
"examples": ["infrastructure@example.com"],
|
||||
"minLength": 1
|
||||
},
|
||||
"picture": {
|
||||
"type": "string",
|
||||
"description": "The URL of an image that represents this entity.",
|
||||
"examples": [
|
||||
"https://example.com/groups/bu-infrastructure.jpeg"
|
||||
],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string",
|
||||
"description": "The immediate parent group in the hierarchy, if any. Not all groups must have a parent; the catalog supports multi-root hierarchies. Groups may however not have more than one parent. This field is an entity reference.",
|
||||
"examples": ["ops"],
|
||||
"minLength": 1
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"description": "The immediate child groups of this group in the hierarchy (whose parent field points to this group). The list must be present, but may be empty if there are no child groups. The items are not guaranteed to be ordered in any particular way. The entries of this array are entity references.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"examples": ["backstage", "other"],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "LocationV1alpha1",
|
||||
"description": "A location is a marker that references other places to look for catalog data.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Location",
|
||||
"metadata": {
|
||||
"name": "org-data"
|
||||
},
|
||||
"spec": {
|
||||
"type": "url",
|
||||
"targets": [
|
||||
"http://github.com/myorg/myproject/org-data-dump/catalog-info-staff.yaml",
|
||||
"http://github.com/myorg/myproject/org-data-dump/catalog-info-consultants.yaml"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "Entity"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["spec"],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["Location"]
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The single location type, that's common to the targets specified in the spec. If it is left out, it is inherited from the location type that originally read the entity data.",
|
||||
"examples": ["url"],
|
||||
"minLength": 1
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "A single target as a string. Can be either an absolute path/URL (depending on the type), or a relative path such as ./details/catalog-info.yaml which is resolved relative to the location of this Location entity itself.",
|
||||
"examples": ["./details/catalog-info.yaml"],
|
||||
"minLength": 1
|
||||
},
|
||||
"targets": {
|
||||
"type": "array",
|
||||
"description": "A list of targets as strings. They can all be either absolute paths/URLs (depending on the type), or relative paths such as ./details/catalog-info.yaml which are resolved relative to the location of this Location entity itself.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"./details/catalog-info.yaml",
|
||||
"http://github.com/myorg/myproject/org-data-dump/catalog-info-staff.yaml"
|
||||
],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "ResourceV1alpha1",
|
||||
"description": "A resource describes the infrastructure a system needs to operate, like BigTable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and systems allows to visualize resource footprint, and create tooling around them.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Resource",
|
||||
"metadata": {
|
||||
"name": "artists-db",
|
||||
"description": "Stores artist details"
|
||||
},
|
||||
"spec": {
|
||||
"type": "database",
|
||||
"owner": "artist-relations-team",
|
||||
"system": "artist-engagement-portal"
|
||||
}
|
||||
}
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "Entity"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["spec"],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["Resource"]
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"required": ["type", "owner"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The type of resource.",
|
||||
"examples": ["database", "s3-bucket", "cluster"],
|
||||
"minLength": 1
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "An entity reference to the owner of the resource.",
|
||||
"examples": ["artist-relations-team", "user:john.johnson"],
|
||||
"minLength": 1
|
||||
},
|
||||
"system": {
|
||||
"type": "string",
|
||||
"description": "An entity reference to the system that the resource belongs to.",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "SystemV1alpha1",
|
||||
"description": "A system is a collection of resources and components. The system may expose or consume one or several APIs. It is viewed as abstraction level that provides potential consumers insights into exposed features without needing a too detailed view into the details of all components. This also gives the owning team the possibility to decide about published artifacts and APIs.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "System",
|
||||
"metadata": {
|
||||
"name": "artist-engagement-portal",
|
||||
"description": "Handy tools to keep artists in the loop"
|
||||
},
|
||||
"spec": {
|
||||
"owner": "artist-relations-team",
|
||||
"domain": "artists"
|
||||
}
|
||||
}
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "Entity"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["spec"],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["System"]
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"required": ["owner"],
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "An entity reference to the owner of the component.",
|
||||
"examples": ["artist-relations-team", "user:john.johnson"],
|
||||
"minLength": 1
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "An entity reference to the domain that the system belongs to.",
|
||||
"examples": ["artists"],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "TemplateV1alpha1",
|
||||
"description": "A Template describes a skeleton for use with the Scaffolder. It is used for describing what templating library is supported, and also for documenting the variables that the template requires using JSON Forms Schema.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Template",
|
||||
"metadata": {
|
||||
"name": "react-ssr-template",
|
||||
"title": "React SSR Template",
|
||||
"description": "Next.js application skeleton for creating isomorphic web applications.",
|
||||
"tags": ["recommended", "react"]
|
||||
},
|
||||
"spec": {
|
||||
"owner": "artist-relations-team",
|
||||
"templater": "cookiecutter",
|
||||
"type": "website",
|
||||
"path": ".",
|
||||
"schema": {
|
||||
"required": ["component-id", "description"],
|
||||
"properties": {
|
||||
"component_id": {
|
||||
"title": "Name",
|
||||
"type": "string",
|
||||
"description": "Unique name of the component"
|
||||
},
|
||||
"description": {
|
||||
"title": "Description",
|
||||
"type": "string",
|
||||
"description": "Description of the component"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "Entity"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["spec"],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["Template"]
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The nice display name for the template. This field is required as is used to reference the template to the user instead of the metadata.name field.",
|
||||
"examples": ["React SSR Template"],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"required": ["type", "templater", "schema"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The type of component. This field is optional but recommended. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.",
|
||||
"examples": ["service", "website", "library"],
|
||||
"minLength": 1
|
||||
},
|
||||
"templater": {
|
||||
"type": "string",
|
||||
"description": "The templating library that is supported by the template skeleton.",
|
||||
"examples": ["cookiecutter"],
|
||||
"minLength": 1
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The string location where the templater should be run if it is not on the same level as the template.yaml definition.",
|
||||
"examples": ["./cookiecutter/skeleton"],
|
||||
"minLength": 1
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"description": "The JSONSchema describing the inputs for the template."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "UserV1alpha1",
|
||||
"description": "A user describes a person, such as an employee, a contractor, or similar. Users belong to Group entities in the catalog. These catalog user entries are connected to the way that authentication within the Backstage ecosystem works. See the auth section of the docs for a discussion of these concepts.",
|
||||
"examples": [
|
||||
{
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "User",
|
||||
"metadata": {
|
||||
"name": "jdoe"
|
||||
},
|
||||
"spec": {
|
||||
"profile": {
|
||||
"displayName": "Jenny Doe",
|
||||
"email": "jenny-doe@example.com",
|
||||
"picture": "https://example.com/staff/jenny-with-party-hat.jpeg"
|
||||
},
|
||||
"memberOf": ["team-b", "employees"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "Entity"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["spec"],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["User"]
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"required": ["memberOf"],
|
||||
"properties": {
|
||||
"profile": {
|
||||
"type": "object",
|
||||
"description": "Optional profile information about the user, mainly for display purposes. All fields of this structure are also optional. The email would be a primary email of some form, that the user may wish to be used for contacting them. The picture is expected to be a URL pointing to an image that's representative of the user, and that a browser could fetch and render on a profile page or similar.",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "A simple display name to present to users.",
|
||||
"examples": ["Jenny Doe"],
|
||||
"minLength": 1
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "An email where this user can be reached.",
|
||||
"examples": ["jenny-doe@example.com"],
|
||||
"minLength": 1
|
||||
},
|
||||
"picture": {
|
||||
"type": "string",
|
||||
"description": "The URL of an image that represents this user.",
|
||||
"examples": [
|
||||
"https://example.com/staff/jenny-with-party-hat.jpeg"
|
||||
],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"memberOf": {
|
||||
"type": "array",
|
||||
"description": "The list of groups that the user is a direct member of (i.e., no transitive memberships are listed here). The list must be present, but may be empty if the user is not member of any groups. The items are not guaranteed to be ordered in any particular way. The entries of this array are entity references.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"examples": ["team-b", "employees"],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user