Merge remote-tracking branch 'origin/master' into erikengervall/plugin-release-manager-as-a-service

This commit is contained in:
Erik Engervall
2021-04-16 09:58:22 +02:00
199 changed files with 5528 additions and 1146 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-sonarqube': patch
---
Export isSonarQubeAvailable.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Add built-in publish action for creating GitHub pull requests.
-30
View File
@@ -1,30 +0,0 @@
---
'@backstage/create-app': patch
---
Fixing dependency resolution for problematic library `graphql-language-service-interface`.
This change might not have to be applied to your local installation, however if you run into this error:
```
Error: Failed to compile.
/tmp/backstage-e2e-uMeycm/test-app/node_modules/graphql-language-service-interface/esm/GraphQLLanguageService.js 100:23
Module parse failed: Unexpected token (100:23)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| }
| let customRules = null;
> if (extensions?.customValidationRules &&
| typeof extensions.customValidationRules === 'function') {
| customRules = extensions.customValidationRules(this._graphQLConfig);
```
You can fix it by adding the following to the root `package.json`.
```json
...
"resolutions": {
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
},
...
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Fix for the `file://` protocol check in the `FilePreparer` being too strict, breaking Windows.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
Add support for configure an LDAP query filter on multiple lines.
-31
View File
@@ -1,31 +0,0 @@
---
'@backstage/create-app': patch
'@backstage/plugin-scaffolder': minor
---
Expose the catalog-import route as an external route from the scaffolder.
This will make it possible to hide the "Register Existing Component" button
when you for example are running backstage with `catalog.readonly=true`.
As a consequence of this change you need add a new binding to your createApp call to
keep the button visible. However, if you instead want to hide the button you can safely
ignore the following example.
To bind the external route from the catalog-import plugin to the scaffolder template
index page, make sure you have the appropriate imports and add the following
to the createApp call:
```typescript
import { catalogImportPlugin } from '@backstage/plugin-catalog-import';
const app = createApp({
// ...
bindRoutes({ bind }) {
// ...
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
});
},
});
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
Exported SignInProviderConfig to strongly type SignInPage providers
-14
View File
@@ -1,14 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Bitbucket server needs username to be set as well as the token or appPassword for the publishing process to work.
```yaml
integrations:
bitbucket:
- host: bitbucket.mycompany.com
apiBaseUrl: https://bitbucket.mycompany.com/rest/api/1.0
token: token
username: username
```
-13
View File
@@ -1,13 +0,0 @@
---
'@backstage/catalog-model': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog': patch
---
Implemented missing support for the dependsOn/dependencyOf relationships
between `Component` and `Resource` catalog model objects.
Added support for generating the relevant relationships to the
`BuiltinKindsEntityProcessor`, and added simple support for fetching
relationships between `Components` and `Resources` for rendering in the
system diagram. All catalog-model changes backwards compatible.
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/plugin-techdocs-backend': patch
---
Change the response status of metadata endpoints in case a documentation is not
available to `404 NOT FOUND`. This also introduces the JSON based error messages
used by other backends.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Adds a collaborator field to GitHub publish action for multiple users and access levels
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Add borders to TechDocs tables and increase font size. Fixes #5264 and #5276.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Add a test id to the shadow root element of the Reader to access it easily in e2e tests
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/core': patch
'@backstage/plugin-api-docs': patch
---
add debounce time attribute for apis-docs for search, giving more time to the users when they are typing.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core': patch
---
exposes undocumented `PageTheme`
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core': patch
---
Don't use a drag & drop cursor when clicking on disabled `IconLinkVertical`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Fix bug in pull request sample template.
+1
View File
@@ -114,6 +114,7 @@ dataflow
deadnaming
debounce
declaratively
deps
destructured
dev
devops
+3 -6
View File
@@ -1,13 +1,10 @@
app:
title: Backstage Tugboat Preview
baseUrl:
$env: TUGBOAT_DEFAULT_SERVICE_URL
baseUrl: ${TUGBOAT_DEFAULT_SERVICE_URL}
backend:
baseUrl:
$env: TUGBOAT_DEFAULT_SERVICE_URL
baseUrl: ${TUGBOAT_DEFAULT_SERVICE_URL}
cors:
origin:
$env: TUGBOAT_DEFAULT_SERVICE_URL
origin: ${TUGBOAT_DEFAULT_SERVICE_URL}
methods: [GET, POST, PUT, DELETE]
credentials: true
+56 -112
View File
@@ -40,47 +40,40 @@ proxy:
'/circleci/api':
target: https://circleci.com/api/v1.1
headers:
Circle-Token:
$env: CIRCLECI_AUTH_TOKEN
Circle-Token: ${CIRCLECI_AUTH_TOKEN}
'/jenkins/api':
target: http://localhost:8080
headers:
Authorization:
$env: JENKINS_BASIC_AUTH_HEADER
Authorization: ${JENKINS_BASIC_AUTH_HEADER}
'/travisci/api':
target: https://api.travis-ci.com
changeOrigin: true
headers:
Authorization:
$env: TRAVISCI_AUTH_TOKEN
Authorization: ${TRAVISCI_AUTH_TOKEN}
travis-api-version: '3'
'/newrelic/apm/api':
target: https://api.newrelic.com/v2
headers:
X-Api-Key:
$env: NEW_RELIC_REST_API_KEY
X-Api-Key: ${NEW_RELIC_REST_API_KEY}
'/pagerduty':
target: https://api.pagerduty.com
headers:
Authorization:
$env: PAGERDUTY_TOKEN
Authorization: ${PAGERDUTY_TOKEN}
'/buildkite/api':
target: https://api.buildkite.com/v2/
headers:
Authorization:
$env: BUILDKITE_TOKEN
Authorization: ${BUILDKITE_TOKEN}
'/sentry/api':
target: https://sentry.io/api/
allowedMethods: ['GET']
headers:
Authorization:
$env: SENTRY_TOKEN
Authorization: ${SENTRY_TOKEN}
organization:
name: My Company
@@ -124,36 +117,28 @@ kafka:
integrations:
github:
- host: github.com
token:
$env: GITHUB_TOKEN
token: ${GITHUB_TOKEN}
### Example for how to add your GitHub Enterprise instance using the API:
# - host: ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token:
# $env: GHE_TOKEN
# token: ${GHE_TOKEN}
### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional):
# - host: ghe.example.net
# rawBaseUrl: https://ghe.example.net/raw
# token:
# $env: GHE_TOKEN
# token: ${GHE_TOKEN}
gitlab:
- host: gitlab.com
token:
$env: GITLAB_TOKEN
token: ${GITLAB_TOKEN}
bitbucket:
- host: bitbucket.org
username:
$env: BITBUCKET_USERNAME
appPassword:
$env: BITBUCKET_APP_PASSWORD
username: ${BITBUCKET_USERNAME}
appPassword: ${BITBUCKET_APP_PASSWORD}
azure:
- host: dev.azure.com
token:
$env: AZURE_TOKEN
token: ${AZURE_TOKEN}
# googleGcs:
# clientEmail: 'example@example.com'
# privateKey:
# $env: GCS_PRIVATE_KEY
# privateKey: ${GCS_PRIVATE_KEY}
catalog:
rules:
@@ -172,21 +157,18 @@ catalog:
githubOrg:
providers:
- target: https://github.com
token:
$env: GITHUB_TOKEN
token: ${GITHUB_TOKEN}
#### Example for how to add your GitHub Enterprise instance using the API:
# - target: https://ghe.example.net
# apiBaseUrl: https://ghe.example.net/api
# token:
# $env: GHE_TOKEN
# token: ${GHE_TOKEN}
ldapOrg:
### Example for how to add your enterprise LDAP server
# providers:
# - target: ldaps://ds.example.net
# bind:
# dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net
# secret:
# $env: LDAP_SECRET
# secret: ${LDAP_SECRET}
# users:
# dn: ou=people,ou=example,dc=example,dc=net
# options:
@@ -202,12 +184,9 @@ catalog:
#providers:
# - target: https://graph.microsoft.com/v1.0
# authority: https://login.microsoftonline.com
# tenantId:
# $env: MICROSOFT_GRAPH_TENANT_ID
# clientId:
# $env: MICROSOFT_GRAPH_CLIENT_ID
# clientSecret:
# $env: MICROSOFT_GRAPH_CLIENT_SECRET_TOKEN
# tenantId: ${MICROSOFT_GRAPH_TENANT_ID}
# clientId: ${MICROSOFT_GRAPH_CLIENT_ID}
# clientSecret: ${MICROSOFT_GRAPH_CLIENT_SECRET_TOKEN}
# userFilter: accountEnabled eq true and userType eq 'member'
# groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified')
@@ -255,27 +234,22 @@ catalog:
scaffolder:
github:
token:
$env: GITHUB_TOKEN
token: ${GITHUB_TOKEN}
visibility: public # or 'internal' or 'private'
gitlab:
api:
baseUrl: https://gitlab.com
token:
$env: GITLAB_TOKEN
token: ${GITLAB_TOKEN}
visibility: public # or 'internal' or 'private'
azure:
baseUrl: https://dev.azure.com/{your-organization}
api:
token:
$env: AZURE_TOKEN
token: ${AZURE_TOKEN}
bitbucket:
api:
host: https://bitbucket.org
username:
$env: BITBUCKET_USERNAME
token:
$env: BITBUCKET_TOKEN
username: ${BITBUCKET_USERNAME}
token: ${BITBUCKET_TOKEN}
visibility: public # or or 'private'
auth:
@@ -286,89 +260,59 @@ auth:
providers:
google:
development:
clientId:
$env: AUTH_GOOGLE_CLIENT_ID
clientSecret:
$env: AUTH_GOOGLE_CLIENT_SECRET
clientId: ${AUTH_GOOGLE_CLIENT_ID}
clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET}
github:
development:
clientId:
$env: AUTH_GITHUB_CLIENT_ID
clientSecret:
$env: AUTH_GITHUB_CLIENT_SECRET
enterpriseInstanceUrl:
$env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL}
gitlab:
development:
clientId:
$env: AUTH_GITLAB_CLIENT_ID
clientSecret:
$env: AUTH_GITLAB_CLIENT_SECRET
audience:
$env: GITLAB_BASE_URL
clientId: ${AUTH_GITLAB_CLIENT_ID}
clientSecret: ${AUTH_GITLAB_CLIENT_SECRET}
audience: ${GITLAB_BASE_URL}
saml:
entryPoint: 'http://localhost:7001/'
issuer: 'passport-saml'
okta:
development:
clientId:
$env: AUTH_OKTA_CLIENT_ID
clientSecret:
$env: AUTH_OKTA_CLIENT_SECRET
audience:
$env: AUTH_OKTA_AUDIENCE
clientId: ${AUTH_OKTA_CLIENT_ID}
clientSecret: ${AUTH_OKTA_CLIENT_SECRET}
audience: ${AUTH_OKTA_AUDIENCE}
oauth2:
development:
clientId:
$env: AUTH_OAUTH2_CLIENT_ID
clientSecret:
$env: AUTH_OAUTH2_CLIENT_SECRET
authorizationUrl:
$env: AUTH_OAUTH2_AUTH_URL
tokenUrl:
$env: AUTH_OAUTH2_TOKEN_URL
clientId: ${AUTH_OAUTH2_CLIENT_ID}
clientSecret: ${AUTH_OAUTH2_CLIENT_SECRET}
authorizationUrl: ${AUTH_OAUTH2_AUTH_URL}
tokenUrl: ${AUTH_OAUTH2_TOKEN_URL}
###
# provide a list of scopes as needed for your OAuth2 Server:
#
# scope: saml-login-selector openid profile email
oidc:
development:
metadataUrl:
$env: AUTH_OIDC_METADATA_URL
clientId:
$env: AUTH_OIDC_CLIENT_ID
clientSecret:
$env: AUTH_OIDC_CLIENT_SECRET
authorizationUrl:
$env: AUTH_OIDC_AUTH_URL
tokenUrl:
$env: AUTH_OIDC_TOKEN_URL
tokenSignedResponseAlg:
$env: AUTH_OIDC_TOKEN_SIGNED_RESPONSE_ALG
metadataUrl: ${AUTH_OIDC_METADATA_URL}
clientId: ${AUTH_OIDC_CLIENT_ID}
clientSecret: ${AUTH_OIDC_CLIENT_SECRET}
authorizationUrl: ${AUTH_OIDC_AUTH_URL}
tokenUrl: ${AUTH_OIDC_TOKEN_URL}
tokenSignedResponseAlg: ${AUTH_OIDC_TOKEN_SIGNED_RESPONSE_ALG}
auth0:
development:
clientId:
$env: AUTH_AUTH0_CLIENT_ID
clientSecret:
$env: AUTH_AUTH0_CLIENT_SECRET
domain:
$env: AUTH_AUTH0_DOMAIN
clientId: ${AUTH_AUTH0_CLIENT_ID}
clientSecret: ${AUTH_AUTH0_CLIENT_SECRET}
domain: ${AUTH_AUTH0_DOMAIN}
microsoft:
development:
clientId:
$env: AUTH_MICROSOFT_CLIENT_ID
clientSecret:
$env: AUTH_MICROSOFT_CLIENT_SECRET
tenantId:
$env: AUTH_MICROSOFT_TENANT_ID
clientId: ${AUTH_MICROSOFT_CLIENT_ID}
clientSecret: ${AUTH_MICROSOFT_CLIENT_SECRET}
tenantId: ${AUTH_MICROSOFT_TENANT_ID}
onelogin:
development:
clientId:
$env: AUTH_ONELOGIN_CLIENT_ID
clientSecret:
$env: AUTH_ONELOGIN_CLIENT_SECRET
issuer:
$env: AUTH_ONELOGIN_ISSUER
clientId: ${AUTH_ONELOGIN_CLIENT_ID}
clientSecret: ${AUTH_ONELOGIN_CLIENT_SECRET}
issuer: ${AUTH_ONELOGIN_ISSUER}
costInsights:
engineerCost: 200000
products:
+21 -42
View File
@@ -127,68 +127,47 @@ appConfig:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$env: AUTH_GOOGLE_CLIENT_ID
clientSecret:
$env: AUTH_GOOGLE_CLIENT_SECRET
clientId: ${AUTH_GOOGLE_CLIENT_ID}
clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET}
github:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$env: AUTH_GITHUB_CLIENT_ID
clientSecret:
$env: AUTH_GITHUB_CLIENT_SECRET
enterpriseInstanceUrl:
$env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL}
gitlab:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$env: AUTH_GITLAB_CLIENT_ID
clientSecret:
$env: AUTH_GITLAB_CLIENT_SECRET
audience:
$env: GITLAB_BASE_URL
clientId: ${AUTH_GITLAB_CLIENT_ID}
clientSecret: ${AUTH_GITLAB_CLIENT_SECRET}
audience: ${GITLAB_BASE_URL}
okta:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$env: AUTH_OKTA_CLIENT_ID
clientSecret:
$env: AUTH_OKTA_CLIENT_SECRET
audience:
$env: AUTH_OKTA_AUDIENCE
clientId: ${AUTH_OKTA_CLIENT_ID}
clientSecret: ${AUTH_OKTA_CLIENT_SECRET}
audience: ${AUTH_OKTA_AUDIENCE}
oauth2:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$env: AUTH_OAUTH2_CLIENT_ID
clientSecret:
$env: AUTH_OAUTH2_CLIENT_SECRET
authorizationURL:
$env: AUTH_OAUTH2_AUTH_URL
tokenURL:
$env: AUTH_OAUTH2_TOKEN_URL
clientId: ${AUTH_OAUTH2_CLIENT_ID}
clientSecret: ${AUTH_OAUTH2_CLIENT_SECRET}
authorizationURL: ${AUTH_OAUTH2_AUTH_URL}
tokenURL: ${AUTH_OAUTH2_TOKEN_URL}
auth0:
development:
clientId:
$env: AUTH_AUTH0_CLIENT_ID
clientSecret:
$env: AUTH_AUTH0_CLIENT_SECRET
domain:
$env: AUTH_AUTH0_DOMAIN
clientId: ${AUTH_AUTH0_CLIENT_ID}
clientSecret: ${AUTH_AUTH0_CLIENT_SECRET}
domain: ${AUTH_AUTH0_DOMAIN}
microsoft:
development:
clientId:
$env: AUTH_MICROSOFT_CLIENT_ID
clientSecret:
$env: AUTH_MICROSOFT_CLIENT_SECRET
tenantId:
$env: AUTH_MICROSOFT_TENANT_ID
clientId: ${AUTH_MICROSOFT_CLIENT_ID}
clientSecret: ${AUTH_MICROSOFT_CLIENT_SECRET}
tenantId: ${AUTH_MICROSOFT_TENANT_ID}
auth:
google:
+14 -27
View File
@@ -95,40 +95,27 @@ auth:
providers:
google:
development:
clientId:
$env: AUTH_GOOGLE_CLIENT_ID
clientSecret:
$env: AUTH_GOOGLE_CLIENT_SECRET
clientId: ${AUTH_GOOGLE_CLIENT_ID}
clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET}
github:
development:
clientId:
$env: AUTH_GITHUB_CLIENT_ID
clientSecret:
$env: AUTH_GITHUB_CLIENT_SECRET
enterpriseInstanceUrl:
$env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL}
gitlab:
development:
clientId:
$env:
clientId: ${AUTH_GITLAB_CLIENT_ID}
oauth2:
development:
clientId:
$env: AUTH_OAUTH2_CLIENT_ID
clientSecret:
$env: AUTH_OAUTH2_CLIENT_SECRET
authorizationUrl:
$env: AUTH_OAUTH2_AUTH_URL
tokenUrl:
$env: AUTH_OAUTH2_TOKEN_URL
scope:
$env: AUTH_OAUTH2_SCOPE
clientId: ${AUTH_OAUTH2_CLIENT_ID}
clientSecret: ${AUTH_OAUTH2_CLIENT_SECRET}
authorizationUrl: ${AUTH_OAUTH2_AUTH_URL}
tokenUrl: ${AUTH_OAUTH2_TOKEN_URL}
scope: ${AUTH_OAUTH2_SCOPE}
saml:
entryPoint:
$env: AUTH_SAML_ENTRY_POINT
issuer:
$env: AUTH_SAML_ISSUER
...
entryPoint: ${AUTH_SAML_ENTRY_POINT}
issuer: ${AUTH_SAML_ISSUER}
...
```
## Implementing Your Own Auth Wrapper
+3 -3
View File
@@ -58,7 +58,7 @@ the local `auth.environment` setting will be selected.
After configuring an authentication provider, the `app` frontend package needs a
small update to show this provider as a login option. The `SignInPage` component
handles this, and takes either a `provider` or `providers` (array) prop of
`SignInConfig` definitions.
`SignInProviderConfig` definitions.
These reference the [ApiRef](../reference/utility-apis/README.md) exported by
the provider. Again, an example using GitHub that can be adapted to any of the
@@ -66,9 +66,9 @@ built-in providers:
```diff
# packages/app/src/App.tsx
+ import { githubAuthApiRef, SignInConfig, SignInPage } from '@backstage/core';
+ import { githubAuthApiRef, SignInProviderConfig, SignInPage } from '@backstage/core';
+ const githubProvider: SignInConfig = {
+ const githubProvider: SignInProviderConfig = {
+ id: 'github-auth-provider',
+ title: 'GitHub',
+ message: 'Sign in using GitHub',
+3
View File
@@ -129,6 +129,9 @@ variable.
$env: MY_SECRET
```
Note however, that it's often more convenient to use
[environment variable substitution](#environment-variable-substitution) instead.
### File Includes
This reads a string value from the entire contents of a text file. The file path
@@ -1,14 +1,15 @@
---
id: deployment-docker
title: Docker
description: Documentation on how to deploy Backstage as a Docker image
id: docker
title: Building a Docker image
sidebar_label: Docker
description: How to build a Backstage Docker image for deployment
---
This section describes how to build a Backstage App into a deployable Docker
image. It is split into three sections, first covering the host build approach,
which is recommended due its speed and more efficient and often simpler caching.
The second section covers a full multi-stage Docker build, and the last section
covers how to split frontend content into a separate image.
covers how to deploy the frontend and backend as separate images.
Something that goes for all of these docker deployment strategies is that they
are stateless, so for a production deployment you will want to set up and
@@ -20,10 +21,10 @@ bundled and served from the backend. This is done using the
`@backstage/plugin-app-backend` plugin, which also injects the frontend
configuration into the app. This means you that you only need to build and
deploy a single container in a minimal setup of Backstage. If you wish to
separate the serving of the frontend out from the backend, see
[the section on that topic below](#separate-frontend).
separate the serving of the frontend out from the backend, see the
[separate frontend](#separate-frontend) topic below.
### Host Build
## Host Build
This section describes how to build a Docker image from a Backstage repo with
most of the build happening outside of Docker. This is almost always the faster
@@ -109,7 +110,7 @@ docker run -it -p 7000:7000 backstage
You should then start to get logs in your terminal, and then you can open your
browser at `http://localhost:7000`
### Multistage Build
## Multi-stage Build
This section describes how to set up a multi-stage Docker build that builds the
entire project within Docker. This is typically slower than a host build, but is
@@ -207,7 +208,7 @@ docker run -it -p 7000:7000 backstage
You should then start to get logs in your terminal, and then you can open your
browser at `http://localhost:7000`
### Separate Frontend
## Separate Frontend
It is sometimes desirable to serve the frontend separately from the backend,
either from a separate image or for example a static file serving provider. The
@@ -1,8 +1,8 @@
---
id: deployment-helm
title: Helm
description: Documentation on Kubernetes and Helm Deployment
sidebar_label: Kubernetes and Helm
id: helm
title: Deploying Backstage with Helm
description: How to deploy Backstage with Helm and Kubernetes
sidebar_label: Helm
---
# Helm charts
+44
View File
@@ -0,0 +1,44 @@
---
id: heroku
title: Deploying with Heroku
sidebar_label: Heroku
description: How to deploy Backstage to Heroku
---
Heroku is a Platform as a Service (PaaS) designed to handle application
deployment in a hands-off way. Heroku supports container deployment of Docker
images, a natural fit for Backstage.
## Configuring the CLI
First, install the
[heroku-cli](https://devcenter.heroku.com/articles/heroku-cli) and login:
```shell
$ heroku login
```
Heroku runs a container registry on `registry.heroku.com`. To push Backstage
Docker images, log in to the container registry also:
```shell
$ heroku container:login
```
You _might_ also need to set your Heroku app's stack to `container`:
```bash
$ heroku stack:set container -a <your-app>
```
## Push and deploy a Docker image
Now we can push a Backstage [Docker image](docker.md) to Heroku's container
registry and release it to the `web` worker:
```bash
$ heroku container:push web -a <your-app>
$ heroku container:release web -a <your-app>
```
Now you should have Backstage up and running! 🎉
+40
View File
@@ -0,0 +1,40 @@
---
id: index
title: Deploying Backstage
sidebar_title: Overview
description: Packaging Backstage and deploying to production
---
Backstage provides tooling to build Docker images, but can be deployed with or
without Docker on many different infrastructures. The _best_ way to deploy
Backstage is in _the same way_ you deploy other software at your organization.
This documentation shows common examples that may be useful when deploying
Backstage for the first time, or for those without established deployment
practices.
> Note: The _easiest_ way to explore Backstage is to visit the
> [live demo site](https://demo.backstage.io).
At Spotify, we deploy software generally by:
1. Building a Docker image
2. Storing the Docker image on a container registry
3. Referencing the image in a Kubernetes Deployment YAML
4. Applying that Deployment to a Kubernetes cluster
This method is covered in [Building a Docker image](docker.md) and
[Deploying with Kubernetes](k8s.md).
There is also an example of deploying on [Heroku](heroku.md), which only
requires the first two steps.
An example of deploying Backstage with a [Helm chart](helm.md), a common pattern
in AWS, is also available.
Please consider contributing other deployment guides if you get Backstage set up
on common infrastructure, it would be a great benefit to the community.
If you need to run Backstage behind a corporate proxy, this
[contributed guide](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md)
may help.
+519
View File
@@ -0,0 +1,519 @@
---
id: k8s
title: Deploying with Kubernetes
sidebar_label: Kubernetes
description: How to deploy Backstage to a Kubernetes cluster
---
[Kubernetes](https://kubernetes.io/) is a system for deploying, scaling and
managing containerized applications. Backstage is designed to fit this model and
run as a stateless application with an external PostgreSQL database.
There are many different tools and patterns for Kubernetes clusters, so the best
way to deploy to an existing Kubernetes setup is _the same way_ you deploy
everything else.
This guide covers basic Kubernetes definitions needed to get Backstage up and
running in a typical cluster. The object definitions might look familiar, since
the Backstage software catalog
[also uses](../features/software-catalog/descriptor-format.md) the Kubernetes
object format for its entity definition files!
## Testing locally
To test out these concepts locally before deploying to a production Kubernetes
cluster, first install [kubectl](https://kubernetes.io/docs/tasks/tools/), the
Kubernetes command-line tool.
Next, install [minikube](https://minikube.sigs.k8s.io/docs/start/). This creates
a single-node Kubernetes cluster on your local machine:
```shell
# Assumes Mac + Homebrew; see the minikube site for other installations
$ brew install minikube
$ minikube start
...
Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default.
```
Now you can run `kubectl` commands and have changes applied to the minikube
cluster. You should be able to see the `kube-system` Kubernetes pods running:
```shell
$ kubectl get pods -A
```
When you're done with the tutorial, use `minikube stop` to halt the cluster and
free up resources.
## Creating a namespace
Deployments in Kubernetes are commonly assigned to their own
[namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/)
to isolate services in a multi-tenant environment.
This can be done through `kubectl` directly:
```shell
$ kubectl create namespace backstage
namespace/backstage created
```
Alternatively, create and apply a Namespace definition:
```yaml
# kubernetes/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: backstage
```
```shell
$ kubectl apply -f kubernetes/namespace.yaml
namespace/backstage created
```
## Creating the PostgreSQL database
Backstage in production uses PostgreSQL as a database. To isolate the database
from Backstage app deployments, we can create a separate Kubernetes deployment
for PostgreSQL.
### Creating a PostgreSQL secret
First, create a Kubernetes Secret for the PostgreSQL username and password. This
will be used by both the PostgreSQL database and Backstage deployments:
```yaml
# kubernetes/postgres-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: postgres-secrets
namespace: backstage
type: Opaque
data:
POSTGRES_USER: YmFja3N0YWdl
POSTGRES_PASSWORD: aHVudGVyMg==
```
The data in Kubernetes secrets are base64-encoded. The values can be generated
on the command line:
```shell
$ echo -n "backstage" | base64
YmFja3N0YWdl
```
> Note: Secrets are base64-encoded, but not encrypted. Be sure to enable
> [Encryption at Rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/)
> for the cluster. For storing secrets in Git, consider
> [SealedSecrets or other solutions](https://learnk8s.io/kubernetes-secrets-in-git).
The secrets can now be applied to the Kubernetes cluster:
```shell
$ kubectl apply -f kubernetes/postgres-secrets.yaml
secret/postgres-secrets created
```
### Creating a PostgreSQL persistent volume
PostgreSQL needs a persistent volume to store data; we'll create one along with
a `PersistentVolumeClaim`. In this case, we're claiming the whole volume - but
claims can ask for only part of a volume as well.
```yaml
# kubernetes/postgres-storage.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-storage
namespace: backstage
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 2G
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: '/mnt/data'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-storage-claim
namespace: backstage
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2G
```
This file contains definitions for two different kinds, separated by a line with
a triple dash. This syntax is helpful if you want to consolidate related
Kubernetes definitions in a single file and apply them at the same time.
Note the volume `type: local`; this creates a volume using local disk on
Kubernetes nodes. More likely in a production scenario, you'd want to use a more
highly available
[type of PersistentVolume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes).
Apply the storage volume and claim to the Kubernetes cluster:
```shell
$ kubectl apply -f kubernetes/postgres-storage.yaml
persistentvolume/postgres-storage created
persistentvolumeclaim/postgres-storage-claim created
```
### Creating a PostgreSQL deployment
Now we can create a Kubernetes Deployment descriptor for the PostgreSQL database
deployment itself:
```yaml
# kubernetes/postgres.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: backstage
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:13.2-alpine
imagePullPolicy: 'IfNotPresent'
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: postgres-secrets
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgresdb
volumes:
- name: postgresdb
persistentVolumeClaim:
claimName: postgres-storage-claim
```
If you're not used to Kubernetes, this is a lot to take in. We're describing a
Deployment (one or more instances of an application) that we'd like Kubernetes
to know about in the `metadata` block.
The `spec` block describes the desired state. Here we've requested Kubernetes
create 1 replica (running instance of PostgreSQL), and to create the replica
with the given pod `template`, which again contains Kubernetes metadata and a
desired state. The template `spec` shows one container, created from the
[published](https://hub.docker.com/_/postgres) `postgres:13.2-alpine` Docker
image.
Note the `envFrom` and `secretRef` - this tells Kubernetes to fill environment
variables in the container with values from the Secret we created. We've also
referenced the volume created for the deployment, and given it the mount path
expected by PostgreSQL.
Apply the PostgreSQL deployment to the Kubernetes cluster:
```shell
$ kubectl apply -f kubernetes/postgres.yaml
deployment.apps/postgres created
$ kubectl get pods --namespace=backstage
NAME READY STATUS RESTARTS AGE
postgres-56c86b8bbc-66pt2 1/1 Running 0 21s
```
Verify the deployment by connecting to the pod:
```shell
$ kubectl exec -it --namespace=backstage postgres-56c86b8bbc-66pt2 -- /bin/bash
bash-5.1# psql -U $POSTGRES_USER
psql (13.2)
backstage=# \q
bash-5.1# exit
```
### Creating a PostgreSQL service
The database pod is running, but how does another pod connect to it?
Kubernetes pods are transient - they can be killed, restarted, or created
dynamically. Therefore we don't want to try to connect to pods directly, but
rather create a Kubernetes Service. Services keep track of pods and direct
traffic to the right place.
The final step for our database is to create the service descriptor:
```yaml
# kubernetes/postgres-service.yaml
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: backstage
spec:
selector:
app: postgres
ports:
- port: 5432
```
Apply the service to the Kubernetes cluster:
```shell
$ kubectl apply -f kubernetes/postgres-service.yaml
service/postgres created
$ kubectl get services --namespace=backstage
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
postgres ClusterIP 10.96.5.103 <none> 5432/TCP 29s
```
## Creating the Backstage instance
Now that we have PostgreSQL up and ready to store data, we can create the
Backstage instance. This follows similar steps as the PostgreSQL deployment.
### Creating a Backstage secret
For any Backstage configuration secrets, such as authorization tokens, we can
create a similar Kubernetes Secret as we did
[for PostgreSQL](#creating-a-postgresql-secret), remembering to base64 encode
the values:
```yaml
# kubernetes/backstage-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: backstage-secrets
namespace: backstage
type: Opaque
data:
GITHUB_TOKEN: VG9rZW5Ub2tlblRva2VuVG9rZW5NYWxrb3ZpY2hUb2tlbg==
```
Apply the secret to the Kubernetes cluster:
```shell
$ kubectl apply -f kubernetes/backstage-secrets.yaml
secret/backstage-secrets created
```
### Creating a Backstage deployment
To create the Backstage deployment, first create a [Docker image](docker.md).
We'll use this image to create a Kubernetes deployment. For this example, we'll
use the standard host build with the frontend bundled and served from the
backend.
First, create a Kubernetes Deployment descriptor:
```yaml
# kubernetes/backstage.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage
namespace: backstage
spec:
replicas: 1
selector:
matchLabels:
app: backstage
template:
metadata:
labels:
app: backstage
spec:
containers:
- name: backstage
image: backstage:1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 7000
envFrom:
- secretRef:
name: postgres-secrets
- secretRef:
name: backstage-secrets
```
For production deployments, the `image` reference will usually be a full URL to
a repository on a container registry (for example, ECR on AWS).
For testing locally with `minikube`, you can point the local Docker daemon to
the `minikube` internal Docker registry and then rebuild the image to install
it:
```shell
$ eval $(minikube docker-env)
$ yarn build-image --tag backstage:1.0.0
```
There is no special wiring needed to access the PostgreSQL service. Since it's
running on the same cluster, Kubernetes will inject `POSTGRES_SERVICE_HOST` and
`POSTGRES_SERVICE_PORT` environment variables into our Backstage container.
These can be used in the Backstage `app-config.yaml` along with the secrets:
```yaml
backend:
database:
client: pg
connection:
host: ${POSTGRES_SERVICE_HOST}
port: ${POSTGRES_SERVICE_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
```
Make sure to rebuild the Docker image after applying `app-config.yaml` changes.
Apply this Deployment to the Kubernetes cluster:
```shell
$ kubectl apply -f kubernetes/backstage.yaml
deployment.apps/backstage created
$ kubectl get deployments --namespace=backstage
NAME READY UP-TO-DATE AVAILABLE AGE
backstage 1/1 1 1 1m
postgres 1/1 1 1 10m
$ kubectl get pods --namespace=backstage
NAME READY STATUS RESTARTS AGE
backstage-54bfcd6476-n2jkm 1/1 Running 0 58s
postgres-56c86b8bbc-66pt2 1/1 Running 0 9m
```
Beautiful! 🎉 The deployment and pod are running in the cluster. If you run into
any trouble, check the container logs from the pod:
```shell
# -f to tail, <pod> -c <container>
$ kubectl logs --namespace=backstage -f backstage-54bfcd6476-n2jkm -c backstage
```
### Creating a Backstage service
Like the [PostgreSQL service](#creating-a-postgresql-service) above, we need to
create a Kubernetes Service for Backstage to handle connecting requests to the
correct pods.
Create the Kubernetes Service descriptor:
```yaml
# kubernetes/backstage-service.yaml
apiVersion: v1
kind: Service
metadata:
name: backstage
namespace: backstage
spec:
selector:
app: backstage
ports:
- name: http
port: 80
targetPort: http
```
The `selector` here is telling the Service which pods to target, and the port
mapping translates normal HTTP port 80 to the backend http port (7000) on the
pod.
Apply this Service to the Kubernetes cluster:
```shell
$ kubectl apply -f kubernetes/backstage-service.yaml
service/backstage created
```
Now we have a fully operational Backstage deployment! 🎉 For a _**grand
reveal**_, you can forward a local port to the service:
```shell
$ sudo kubectl port-forward --namespace=backstage svc/backstage 80:80
Forwarding from 127.0.0.1:80 -> 7000
```
This shows port 7000 since `port-forward` doesn't _really_ support services, so
it cheats by looking up the first pod for a service and connecting to the mapped
pod port.
Note that `app.baseUrl` and `backend.baseUrl` in your `app-config.yaml` should
match what we're forwarding here (port omitted in this example since we're using
the default HTTP port 80):
```yaml
# app-config.yaml
app:
baseUrl: http://localhost
organization:
name: Spotify
backend:
baseUrl: http://localhost
listen:
port: 7000
cors:
origin: http://localhost
```
If you're using an [auth provider](../auth/index.md), it should also have this
address configured for the authentication pop-up to work properly.
Now you can open a browser on your machine to [localhost](http://localhost) and
browse your Kubernetes-deployed Backstage instance. 🚢🚢🚢
## Further steps
This is most of the way to a full production deployment of Backstage on
Kubernetes. There's a few additional steps to that will likely be needed beyond
the scope of this document.
### Set up a more reliable volume
The `PersistentVolume` configured above uses `local` Kubernetes node storage.
This should be replaced with a cloud volume, network attached storage, or
something more persistent beyond a Kubernetes node.
### Expose the Backstage service
The Kubernetes Service is not exposed for external connections from outside the
cluster. This is generally done with a Kubernetes
[ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) or
an
[external load balancer](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/).
### Update the Deployment image
To update the Kubernetes deployment to a newly published version of your
Backstage Docker image, update the image tag reference in `backstage.yaml` and
then apply the changes with `kubectl apply -f kubernetes/backstage.yaml`.
For production purposes, this image tag will generally be a full-fledged URL
pointing to a container registry where built Docker images are hosted. This can
be hosted internally in your infrastructure, or a managed one offered by a cloud
provider.
+1 -2
View File
@@ -25,8 +25,7 @@ kubernetes:
- url: http://127.0.0.1:9999
name: minikube
authProvider: 'serviceAccount'
serviceAccountToken:
$env: K8S_MINIKUBE_TOKEN
serviceAccountToken: ${K8S_MINIKUBE_TOKEN}
- url: http://127.0.0.2:9999
name: aws-cluster-1
authProvider: 'aws'
@@ -156,8 +156,18 @@ The key points to note are:
- Call `emit` any number of times with the results of that process
- Finally return `true`
You should now be able to instantiate this class in your backend, and add it to
the `CatalogBuilder` using the `addProcessors` method.
You should now be able to add this class to your backend in
`packages/backend/src/plugins/catalog.ts`:
```diff
+ import { SystemXReaderProcessor } from '../path/to/class';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = new CatalogBuilder(env);
+ builder.addProcessor(new SystemXReaderProcessor(env.reader));
```
Start up the backend - it should now start reading from the previously
registered location and you'll see your entities start to appear in Backstage.
@@ -92,12 +92,14 @@ view and edit links need changing.
# Example:
metadata:
annotations:
backstage.io/source-location: github:https://github.com/my-org/my-service
backstage.io/source-location: url:https://github.com/my-org/my-service/
```
A `Location` reference that points to the source code of the entity (typically a
`Component`). Useful when catalog files do not get ingested from the source code
repository itself.
repository itself. If the URL points to a folder, it is important that it is
suffixed with a `'/'` in order for relative path resolution to work
consistently.
### jenkins.io/github-folder
@@ -189,8 +189,7 @@ public within the enterprise.
integrations:
github:
- host: github.com
token:
$env: GITHUB_TOKEN
token: ${GITHUB_TOKEN}
scaffolder:
github:
@@ -207,8 +206,7 @@ instance:
integrations:
gitlab:
- host: gitlab.com
token:
$env: GITLAB_TOKEN
token: ${GITLAB_TOKEN}
```
#### Bitbucket
@@ -221,8 +219,7 @@ following:
integrations:
bitbucket:
- host: bitbucket.org
token:
$env: BITBUCKET_TOKEN
token: ${BITBUCKET_TOKEN}
```
or
@@ -231,10 +228,8 @@ or
integrations:
bitbucket:
- host: bitbucket.org
appPassword:
$env: BITBUCKET_APP_PASSWORD
username:
$env: BITBUCKET_USERNAME
appPassword: ${BITBUCKET_APP_PASSWORD}
username: ${BITBUCKET_USERNAME}
```
#### Azure DevOps
@@ -249,8 +244,7 @@ verified.
integrations:
azure:
- host: dev.azure.com
token:
$env: AZURE_TOKEN
token: ${AZURE_TOKEN}
```
### Running the Backend
+6 -12
View File
@@ -65,22 +65,18 @@ techdocs:
# https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html
# https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html
credentials:
accessKeyId:
$env: TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL
secretAccessKey:
$env: TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL
accessKeyId: ${TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL}
secretAccessKey: ${TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL}
# (Optional) AWS Region of the bucket.
# If not set, AWS_REGION environment variable or aws config file will be used.
# https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html
region:
$env: AWS_REGION
region: ${AWS_REGION}
# (Optional) Endpoint URI to send requests to.
# If not set, the default endpoint is built from the configured region.
# https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property
endpoint:
$env: AWS_ENDPOINT
endpoint: ${AWS_ENDPOINT}
# Required when techdocs.publisher.type is set to 'azureBlobStorage'. Skip otherwise.
@@ -91,13 +87,11 @@ techdocs:
# (Required) An account name is required to write to a storage blob container.
# https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
credentials:
accountName:
$env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME
accountName: ${TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME}
# (Optional) An account key is required to write to a storage container.
# If missing,AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET environment variable will be used.
# https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json
accountKey:
$env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY
accountKey: ${TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY}
# (Optional and Legacy) TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc.
# You don't have to specify this anymore.
+15 -30
View File
@@ -95,8 +95,7 @@ techdocs:
type: 'googleGcs'
googleGcs:
bucketName: 'name-of-techdocs-storage-bucket'
credentials:
$env: GOOGLE_APPLICATION_CREDENTIALS
credentials: ${GOOGLE_APPLICATION_CREDENTIALS}
```
**4. That's it!**
@@ -179,13 +178,10 @@ techdocs:
type: 'awsS3'
awsS3:
bucketName: 'name-of-techdocs-storage-bucket'
region:
$env: AWS_REGION
region: ${AWS_REGION}
credentials:
accessKeyId:
$env: AWS_ACCESS_KEY_ID
secretAccessKey:
$env: AWS_SECRET_ACCESS_KEY
accessKeyId: ${AWS_ACCESS_KEY_ID}
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
```
Refer to the
@@ -202,8 +198,7 @@ techdocs:
type: 'awsS3'
awsS3:
bucketName: 'name-of-techdocs-storage-bucket'
region:
$env: AWS_REGION
region: ${AWS_REGION}
credentials:
roleArn: arn:aws:iam::123456789012:role/my-backstage-role
```
@@ -276,8 +271,7 @@ techdocs:
azureBlobStorage:
containerName: 'name-of-techdocs-storage-bucket'
credentials:
accountName:
$env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME
accountName: ${TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME}
```
**3b. Authentication using app-config.yaml**
@@ -297,10 +291,8 @@ techdocs:
azureBlobStorage:
containerName: 'name-of-techdocs-storage-bucket'
credentials:
accountName:
$env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME
accountKey:
$env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY
accountName: ${TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_NAME}
accountKey: ${TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY}
```
**4. That's it!**
@@ -361,20 +353,13 @@ techdocs:
openStackSwift:
containerName: 'name-of-techdocs-storage-bucket'
credentials:
userName:
$env: OPENSTACK_SWIFT_STORAGE_USERNAME
password:
$env: OPENSTACK_SWIFT_STORAGE_PASSWORD
authUrl:
$env: OPENSTACK_SWIFT_STORAGE_AUTH_URL
keystoneAuthVersion:
$env: OPENSTACK_SWIFT_STORAGE_AUTH_VERSION
domainId:
$env: OPENSTACK_SWIFT_STORAGE_DOMAIN_ID
domainName:
$env: OPENSTACK_SWIFT_STORAGE_DOMAIN_NAME
region:
$env: OPENSTACK_SWIFT_STORAGE_REGION
userName: ${OPENSTACK_SWIFT_STORAGE_USERNAME}
password: ${OPENSTACK_SWIFT_STORAGE_PASSWORD}
authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL}
keystoneAuthVersion: ${OPENSTACK_SWIFT_STORAGE_AUTH_VERSION}
domainId: ${OPENSTACK_SWIFT_STORAGE_DOMAIN_ID}
domainName: ${OPENSTACK_SWIFT_STORAGE_DOMAIN_NAME}
region: ${OPENSTACK_SWIFT_STORAGE_REGION}
```
**4. That's it!**
@@ -31,26 +31,33 @@ CircleCI is a frontend UI plugin, it goes in `app` rather than `backend`.
```js
// packages/app/src/plugins.ts
export { plugin as Circleci } from '@backstage/plugin-circleci';
export { plugin as CircleCi } from '@backstage/plugin-circleci';
```
3. Register the plugin router:
3. Register the plugin in the entity pages:
```jsx
// packages/app/src/components/catalog/EntityPage.tsx
```diff
// packages/app/src/components/catalog/EntityPage.tsx
+import {
+ EntityCircleCIContent,
+ isCircleCIAvailable,
+} from '@backstage/plugin-circleci';
import { Router as CircleCIRouter } from '@backstage/plugin-circleci';
// Then somewhere inside <EntityPageLayout>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<CircleCIRouter />}
/>;
...
const cicdContent = (
<EntitySwitch>
...
+ <EntitySwitch.Case if={isCircleCIAvailable}>
+ <EntityCircleCIContent />
+ </EntitySwitch.Case>;
</EntitySwitch>
);
```
Note that stand-alone plugins that are not "attached" to the Software Catalog
would be added outside the `EntityPage`.
This is just one example, but each Backstage instance may integrate content or
cards to suit their needs on different pages, tabs, etc. Note that stand-alone
plugins that are not "attached" to the Software Catalog would be added outside
the `EntityPage`.
4. [Optional] Add proxy config:
@@ -60,8 +67,7 @@ proxy:
'/circleci/api':
target: https://circleci.com/api/v1.1
headers:
Circle-Token:
$env: CIRCLECI_AUTH_TOKEN
Circle-Token: ${CIRCLECI_AUTH_TOKEN}
```
### Adding a plugin page to the Sidebar
-13
View File
@@ -1,13 +0,0 @@
---
id: deployment-k8s
title: Kubernetes
description: Documentation on Kubernetes and K8s Deployment
---
Backstage itself provides tooling up to the point of building Docker images.
Beyond that point we do not have an opinionated way to deploy Backstage within
Kubernetes, as each cluster has its own unique set of tooling and patterns.
We do provide examples to help you get started though. Check out
[this example](https://github.com/backstage/backstage/tree/master/contrib/kubernetes/plain_single_backend_deployment/)
for a basic single-deployment setup.
-35
View File
@@ -1,35 +0,0 @@
---
id: deployment-other
title: Other
description: Documentation on different ways of Deployment
---
## Heroku
Deploying to Heroku is relatively easy following these steps.
First, make sure you have the
[Heroku CLI installed](https://devcenter.heroku.com/articles/heroku-cli) and log
into it as well as login into Heroku's
[container registry](https://devcenter.heroku.com/articles/container-registry-and-runtime).
```bash
$ heroku login
$ heroku container:login
```
You _might_ also need to set your Heroku app's stack to `container`.
```bash
$ heroku stack:set container -a <your-app>
```
We can now build/push the Docker image to Heroku's container registry and
release it to the `web` worker.
```bash
$ heroku container:push web -a <your-app>
$ heroku container:release web -a <your-app>
```
With that, you should have Backstage up and running!
@@ -24,10 +24,8 @@ Explicit credentials can be set in the following format:
```yaml
integrations:
googleGcs:
clientEmail:
$env: GCS_CLIENT_EMAIL
privateKey:
$env: GCS_PRIVATE_KEY
clientEmail: ${GCS_CLIENT_EMAIL}
privateKey: ${GCS_PRIVATE_KEY}
```
Then make sure the environment variables `GCS_CLIENT_EMAIL` and
+3 -2
View File
@@ -40,8 +40,9 @@ proxy:
'/larger-example/v1':
target: http://larger.example.com:8080/svc.v1
headers:
Authorization:
$env: EXAMPLE_AUTH_HEADER
Authorization: ${EXAMPLE_AUTH_HEADER}
# ...or interpolating a value into part of a string,
# Authorization: Bearer ${EXAMPLE_AUTH_TOKEN}
```
Each key under the proxy configuration entry is a route to match, below the
+14 -27
View File
@@ -79,13 +79,10 @@ auth:
providers:
github:
development:
clientId:
$env: AUTH_GITHUB_CLIENT_ID
clientSecret:
$env: AUTH_GITHUB_CLIENT_SECRET
## uncomment the following two lines if using enterprise
# enterpriseInstanceUrl:
# $env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
## uncomment the following line if using enterprise
# enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL}
```
### 2. Generate a GitHub client ID and secret
@@ -122,10 +119,8 @@ auth:
providers:
gitlab:
development:
clientId:
$env: AUTH_GITLAB_CLIENT_ID
clientSecret:
$env: AUTH_GITLAB_CLIENT_SECRET
clientId: ${AUTH_GITLAB_CLIENT_ID}
clientSecret: ${AUTH_GITLAB_CLIENT_SECRET}
audience: https://gitlab.com # Or your self-hosted GitLab instance URL
```
@@ -172,10 +167,8 @@ auth:
providers:
google:
development:
clientId:
$env: AUTH_GOOGLE_CLIENT_ID
clientSecret:
$env: AUTH_GOOGLE_CLIENT_SECRET
clientId: ${AUTH_GOOGLE_CLIENT_ID}
clientSecret: ${AUTH_GOOGLE_CLIENT_SECRET}
```
### 2. Generate Google Credentials in Google Cloud console
@@ -216,12 +209,9 @@ auth:
providers:
microsoft:
development:
clientId:
$env: AUTH_MICROSOFT_CLIENT_ID
clientSecret:
$env: AUTH_MICROSOFT_CLIENT_SECRET
tenantId:
$env: AUTH_MICROSOFT_TENANT_ID
clientId: ${AUTH_MICROSOFT_CLIENT_ID}
clientSecret: ${AUTH_MICROSOFT_CLIENT_SECRET}
tenantId: ${AUTH_MICROSOFT_TENANT_ID}
```
### 2. Create a Microsoft App Registration in Microsoft Portal
@@ -264,12 +254,9 @@ auth:
providers:
auth0:
development:
clientId:
$env: AUTH_AUTH0_CLIENT_ID
clientSecret:
$env: AUTH_AUTH0_CLIENT_SECRET
domain:
$env: AUTH_AUTH0_DOMAIN_ID
clientId: ${AUTH_AUTH0_CLIENT_ID}
clientSecret: ${AUTH_AUTH0_CLIENT_SECRET}
domain: ${AUTH_AUTH0_DOMAIN_ID}
```
### 2. Create an Auth0 application in the Auth0 management console
+7 -11
View File
@@ -38,14 +38,10 @@ backend:
+ # config options: https://node-postgres.com/api/client
+ client: pg
+ connection:
+ host:
+ $env: POSTGRES_HOST
+ port:
+ $env: POSTGRES_PORT
+ user:
+ $env: POSTGRES_USER
+ password:
+ $env: POSTGRES_PASSWORD
+ host: ${POSTGRES_HOST}
+ port: ${POSTGRES_PORT}
+ user: ${POSTGRES_USER}
+ password: ${POSTGRES_PASSWORD}
+ # https://node-postgres.com/features/ssl
+ #ssl: require # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require)
+ #ca: # if you have a CA file and want to verify it you can uncomment this section
@@ -53,9 +49,9 @@ backend:
```
If you have a `app-config.local.yaml` for local development, a similar update
If you have an `app-config.local.yaml` for local development, a similar update
should be made there. You can set the `POSTGRES_` environment variables prior to
launching Backstage, or remove the $env keys and simply set values directly for
development.
launching Backstage, or remove the `${...}` values and simply set actual values
directly for development.
The Backstage App is now ready to start up with a PostgreSQL backing database.
+2 -2
View File
@@ -15,9 +15,9 @@
"verify:sidebars": "node ./scripts/verify-sidebars"
},
"devDependencies": {
"@spotify/prettier-config": "^9.0.0",
"@spotify/prettier-config": "^10.0.0",
"docusaurus": "^2.0.0-alpha.70",
"js-yaml": "^4.0.0",
"js-yaml": "^4.1.0",
"prettier": "^2.2.1"
},
"prettier": "@spotify/prettier-config"
+6 -10
View File
@@ -22,16 +22,6 @@
"getting-started/app-custom-theme"
]
},
{
"type": "subcategory",
"label": "Deployment",
"ids": [
"getting-started/deployment-docker",
"getting-started/deployment-k8s",
"getting-started/deployment-helm",
"getting-started/deployment-other"
]
},
"getting-started/keeping-backstage-updated",
"getting-started/concepts",
"getting-started/contributors"
@@ -211,6 +201,12 @@
"auth/auth-backend-classes",
"auth/glossary"
],
"Deployment": [
"deployment/index",
"deployment/k8s",
"deployment/helm",
"deployment/heroku"
],
"Designing for Backstage": [
"dls/design",
"dls/contributing-to-storybook",
+8 -8
View File
@@ -909,10 +909,10 @@
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
"@spotify/prettier-config@^9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@spotify/prettier-config/-/prettier-config-9.0.0.tgz#7b562d56573c6fc0094446fbc92b22bc318945dc"
integrity sha512-In1q0tIiqTYKAGe3KOHDcFDdZRFISyQeSeipeTHGfki23ebHRZcjxvqj5SSdBkw65D4VpSREMi0s9i5iJiMcTw==
"@spotify/prettier-config@^10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@spotify/prettier-config/-/prettier-config-10.0.0.tgz#fa076d98d2e7e6c53dd3d86a696307a7010bd056"
integrity sha512-VYOdo8P7lIScAkl02nB9KpUAuOYMManryBIBuKJkAw5D3aVtLobfmdIKvdV6MqEmGMEQPbn7w/UpnjJYhUH+IA==
"@types/cheerio@^0.22.8":
version "0.22.23"
@@ -3862,10 +3862,10 @@ js-yaml@^3.13.1, js-yaml@^3.8.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
+25
View File
@@ -1,5 +1,30 @@
# example-app
## 0.2.24
### Patch Changes
- Updated dependencies [a360f9478]
- Updated dependencies [bb5055aee]
- Updated dependencies [d0d1c2f7b]
- Updated dependencies [5d0740563]
- Updated dependencies [12390778e]
- Updated dependencies [fef852ecd]
- Updated dependencies [18f7345a6]
- Updated dependencies [5cafcf452]
- Updated dependencies [86a95ba67]
- Updated dependencies [e27cb6c45]
- Updated dependencies [184b02bef]
- Updated dependencies [60ce64aa2]
- @backstage/plugin-scaffolder@0.9.0
- @backstage/catalog-model@0.7.7
- @backstage/core@0.7.5
- @backstage/plugin-catalog@0.5.4
- @backstage/plugin-api-docs@0.4.11
- @backstage/plugin-techdocs@0.7.2
- @backstage/plugin-tech-radar@0.3.9
- @backstage/cli@0.6.8
## 0.2.23
### Patch Changes
+9 -9
View File
@@ -1,16 +1,16 @@
{
"name": "example-app",
"version": "0.2.23",
"version": "0.2.24",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.7.6",
"@backstage/cli": "^0.6.7",
"@backstage/core": "^0.7.4",
"@backstage/catalog-model": "^0.7.7",
"@backstage/cli": "^0.6.8",
"@backstage/core": "^0.7.5",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-api-docs": "^0.4.9",
"@backstage/plugin-api-docs": "^0.4.11",
"@backstage/plugin-badges": "^0.2.0",
"@backstage/plugin-catalog": "^0.5.3",
"@backstage/plugin-catalog": "^0.5.4",
"@backstage/plugin-catalog-import": "^0.5.2",
"@backstage/plugin-catalog-react": "^0.1.3",
"@backstage/plugin-circleci": "^0.2.12",
@@ -32,11 +32,11 @@
"@backstage/plugin-register-component": "^0.2.12",
"@backstage/plugin-github-release-manager": "^0.1.1",
"@backstage/plugin-rollbar": "^0.3.3",
"@backstage/plugin-scaffolder": "^0.8.2",
"@backstage/plugin-scaffolder": "^0.9.0",
"@backstage/plugin-search": "^0.3.4",
"@backstage/plugin-sentry": "^0.3.8",
"@backstage/plugin-tech-radar": "^0.3.8",
"@backstage/plugin-techdocs": "^0.7.0",
"@backstage/plugin-tech-radar": "^0.3.9",
"@backstage/plugin-techdocs": "^0.7.2",
"@backstage/plugin-todo": "^0.1.0",
"@backstage/plugin-user-settings": "^0.2.8",
"@backstage/theme": "^0.2.5",
+3
View File
@@ -48,6 +48,9 @@ export default async function createPlugin({
discovery,
});
// checks if the publisher is working and logs the result
await publisher.getReadiness();
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
+9
View File
@@ -1,5 +1,14 @@
# @backstage/catalog-client
## 0.3.10
### Patch Changes
- 442f34b87: Make sure the `CatalogClient` escapes URL parameters correctly.
- Updated dependencies [bb5055aee]
- Updated dependencies [5d0740563]
- @backstage/catalog-model@0.7.7
## 0.3.9
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-client",
"version": "0.3.9",
"version": "0.3.10",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,13 +29,13 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.7.5",
"@backstage/catalog-model": "^0.7.7",
"@backstage/config": "^0.1.4",
"@backstage/errors": "^0.1.1",
"cross-fetch": "^3.0.6"
},
"devDependencies": {
"@backstage/cli": "^0.6.5",
"@backstage/cli": "^0.6.8",
"@types/jest": "^26.0.7",
"msw": "^0.21.2"
},
+19 -5
View File
@@ -42,10 +42,14 @@ export class CatalogClient implements CatalogApi {
}
async getLocationById(
id: String,
id: string,
options?: CatalogRequestOptions,
): Promise<Location | undefined> {
return await this.requestOptional('GET', `/locations/${id}`, options);
return await this.requestOptional(
'GET',
`/locations/${encodeURIComponent(id)}`,
options,
);
}
async getEntities(
@@ -86,7 +90,9 @@ export class CatalogClient implements CatalogApi {
const { kind, namespace = 'default', name } = compoundName;
return this.requestOptional(
'GET',
`/entities/by-name/${kind}/${namespace}/${name}`,
`/entities/by-name/${encodeURIComponent(kind)}/${encodeURIComponent(
namespace,
)}/${encodeURIComponent(name)}`,
options,
);
}
@@ -171,14 +177,22 @@ export class CatalogClient implements CatalogApi {
id: string,
options?: CatalogRequestOptions,
): Promise<void> {
await this.requestIgnored('DELETE', `/locations/${id}`, options);
await this.requestIgnored(
'DELETE',
`/locations/${encodeURIComponent(id)}`,
options,
);
}
async removeEntityByUid(
uid: string,
options?: CatalogRequestOptions,
): Promise<void> {
await this.requestIgnored('DELETE', `/entities/by-uid/${uid}`, options);
await this.requestIgnored(
'DELETE',
`/entities/by-uid/${encodeURIComponent(uid)}`,
options,
);
}
//
+1 -1
View File
@@ -46,7 +46,7 @@ export interface CatalogApi {
// Locations
getLocationById(
id: String,
id: string,
options?: CatalogRequestOptions,
): Promise<Location | undefined>;
getOriginLocationByEntity(
+13
View File
@@ -1,5 +1,18 @@
# @backstage/catalog-model
## 0.7.7
### Patch Changes
- bb5055aee: Add getEntitySourceLocation helper
- 5d0740563: Implemented missing support for the dependsOn/dependencyOf relationships
between `Component` and `Resource` catalog model objects.
Added support for generating the relevant relationships to the
`BuiltinKindsEntityProcessor`, and added simple support for fetching
relationships between `Components` and `Resources` for rendering in the
system diagram. All catalog-model changes backwards compatible.
## 0.7.6
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.7.6",
"version": "0.7.7",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -39,7 +39,7 @@
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.6.7",
"@backstage/cli": "^0.6.8",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
@@ -14,7 +14,11 @@
* limitations under the License.
*/
import { parseLocationReference, stringifyLocationReference } from './helpers';
import {
getEntitySourceLocation,
parseLocationReference,
stringifyLocationReference,
} from './helpers';
describe('parseLocationReference', () => {
it('works for the simple case', () => {
@@ -68,3 +72,48 @@ describe('stringifyLocationReference', () => {
).toThrow('Unable to stringify location reference, empty target');
});
});
describe('getEntitySourceLocation', () => {
it('returns the source-location', () => {
expect(
getEntitySourceLocation({
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
name: 'test',
namespace: 'default',
annotations: {
'backstage.io/source-location': 'url:https://backstage.io/foo.yaml',
'backstage.io/managed-by-location': 'url:https://spotify.com',
},
},
}),
).toEqual({ target: 'https://backstage.io/foo.yaml', type: 'url' });
});
it('returns the managed-by-location', () => {
expect(
getEntitySourceLocation({
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
name: 'test',
namespace: 'default',
annotations: {
'backstage.io/managed-by-location': 'url:https://spotify.com',
},
},
}),
).toEqual({ target: 'https://spotify.com', type: 'url' });
});
it('rejects missing location annotation', () => {
expect(() =>
getEntitySourceLocation({
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: { name: 'test', namespace: 'default' },
}),
).toThrow(`Entity 'location:default/test' is missing location`);
});
});
@@ -14,6 +14,9 @@
* limitations under the License.
*/
import { Entity, stringifyEntityRef } from '../entity';
import { LOCATION_ANNOTATION, SOURCE_LOCATION_ANNOTATION } from './annotation';
/**
* Parses a string form location reference.
*
@@ -80,3 +83,26 @@ export function stringifyLocationReference(ref: {
return `${type}:${target}`;
}
/**
* Returns the source code location of the Entity, to the extent that one exists.
*
* If the returned location type is of type 'url', the target should be readable at least
* using the UrlReader from @backstage/backend-common. If it is not of type 'url', the caller
* needs to have explicit handling of each location type or signal that it is not supported.
*/
export function getEntitySourceLocation(
entity: Entity,
): { type: string; target: string } {
const locationRef =
entity.metadata?.annotations?.[SOURCE_LOCATION_ANNOTATION] ??
entity.metadata?.annotations?.[LOCATION_ANNOTATION];
if (!locationRef) {
throw new Error(
`Entity '${stringifyEntityRef(entity)}' is missing location`,
);
}
return parseLocationReference(locationRef);
}
+5 -1
View File
@@ -19,7 +19,11 @@ export {
ORIGIN_LOCATION_ANNOTATION,
SOURCE_LOCATION_ANNOTATION,
} from './annotation';
export { parseLocationReference, stringifyLocationReference } from './helpers';
export {
parseLocationReference,
stringifyLocationReference,
getEntitySourceLocation,
} from './helpers';
export type { Location, LocationSpec } from './types';
export {
analyzeLocationSchema,
+6
View File
@@ -1,5 +1,11 @@
# @backstage/cli
## 0.6.8
### Patch Changes
- 60ce64aa2: Disable hot reloading in CI environments.
## 0.6.7
### Patch Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.6.7",
"version": "0.6.8",
"private": false,
"publishConfig": {
"access": "public"
@@ -118,7 +118,7 @@
"devDependencies": {
"@backstage/backend-common": "^0.6.2",
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.4",
"@backstage/core": "^0.7.5",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@backstage/theme": "^0.2.5",
+1 -1
View File
@@ -43,7 +43,7 @@ export async function serveBundle(options: ServeOptions) {
const compiler = webpack(config);
const server = new WebpackDevServer(compiler, {
hot: true,
hot: !process.env.CI,
contentBase: paths.targetPublic,
contentBasePublicPath: config.output?.publicPath,
publicPath: config.output?.publicPath,
+9
View File
@@ -1,5 +1,14 @@
# @backstage/core
## 0.7.5
### Patch Changes
- d0d1c2f7b: Pass `inverse` prop to Gauge from GaugeCard
- 5cafcf452: add debounce time attribute for apis-docs for search, giving more time to the users when they are typing.
- 86a95ba67: exposes undocumented `PageTheme`
- e27cb6c45: Don't use a drag & drop cursor when clicking on disabled `IconLinkVertical`.
## 0.7.4
### Patch Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.7.4",
"version": "0.7.5",
"private": false,
"publishConfig": {
"access": "public",
@@ -69,7 +69,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.6.7",
"@backstage/cli": "^0.6.8",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
@@ -45,6 +45,9 @@ export const Default = () => (
<Grid item>
<GaugeCard title="Progress" progress={0.89} />
</Grid>
<Grid item>
<GaugeCard title="Progress" inverse progress={0.2} />
</Grid>
</Wrapper>
);
@@ -67,6 +70,14 @@ export const Subhead = () => (
progress={0.89}
/>
</Grid>
<Grid item>
<GaugeCard
title="Progress"
subheader="With a subheader"
inverse
progress={0.2}
/>
</Grid>
</Wrapper>
);
@@ -81,5 +92,8 @@ export const LinkInFooter = () => (
<Grid item>
<GaugeCard title="Progress" deepLink={linkInfo} progress={0.89} />
</Grid>
<Grid item>
<GaugeCard title="Progress" deepLink={linkInfo} inverse progress={0.2} />
</Grid>
</Wrapper>
);
@@ -26,6 +26,7 @@ type Props = {
variant?: InfoCardVariants;
/** Progress in % specified as decimal, e.g. "0.23" */
progress: number;
inverse?: boolean;
deepLink?: BottomLinkProps;
};
@@ -38,7 +39,7 @@ const useStyles = makeStyles({
export const GaugeCard = (props: Props) => {
const classes = useStyles(props);
const { title, subheader, progress, deepLink, variant } = props;
const { title, subheader, progress, inverse, deepLink, variant } = props;
return (
<div className={classes.root}>
@@ -48,7 +49,7 @@ export const GaugeCard = (props: Props) => {
deepLink={deepLink}
variant={variant}
>
<Gauge value={progress} />
<Gauge value={progress} inverse={inverse} />
</InfoCard>
</div>
);
@@ -53,38 +53,6 @@ export const Apis = () => (
</Page>
);
export const Grpc = () => (
<Page themeId="grpc">
<Header title="Grpc catalogue" type="tool">
{labels}
</Header>
</Page>
);
export const AsyncApi = () => (
<Page themeId="asyncapi">
<Header title="Async API catalogue" type="tool">
{labels}
</Header>
</Page>
);
export const Graphql = () => (
<Page themeId="graphql">
<Header title="GraphQL API catalogue" type="tool">
{labels}
</Header>
</Page>
);
export const OpenApi = () => (
<Page themeId="openapi">
<Header title="OpenAPI catalogue" type="tool">
{labels}
</Header>
</Page>
);
export const Tool = () => (
<Page themeId="tool">
<Header title="Stand-alone tool" type="tool">
@@ -22,7 +22,7 @@ import { ContentHeader } from '../ContentHeader/ContentHeader';
import { Grid, Button, Typography } from '@material-ui/core';
import { SignInPageProps, useApi, configApiRef } from '@backstage/core-api';
import { useSignInProviders, getSignInProviders } from './providers';
import { IdentityProviders, SignInConfig } from './types';
import { IdentityProviders, SignInProviderConfig } from './types';
import { Progress } from '../../components/Progress';
import { GridItem, useStyles } from './styles';
import { InfoCard } from '../InfoCard';
@@ -34,7 +34,7 @@ type MultiSignInPageProps = SignInPageProps & {
};
type SingleSignInPageProps = SignInPageProps & {
provider: SignInConfig;
provider: SignInProviderConfig;
auto?: boolean;
};
@@ -21,13 +21,13 @@ import {
ProviderComponent,
ProviderLoader,
SignInProvider,
SignInConfig,
SignInProviderConfig,
} from './types';
import { useApi, errorApiRef } from '@backstage/core-api';
import { GridItem } from './styles';
const Component: ProviderComponent = ({ config, onResult }) => {
const { apiRef, title, message } = config as SignInConfig;
const { apiRef, title, message } = config as SignInProviderConfig;
const authApi = useApi(apiRef);
const errorApi = useApi(errorApiRef);
@@ -14,4 +14,5 @@
* limitations under the License.
*/
export type { SignInProviderConfig } from './types';
export { SignInPage } from './SignInPage';
@@ -22,7 +22,11 @@ import {
useApiHolder,
errorApiRef,
} from '@backstage/core-api';
import { SignInConfig, IdentityProviders, SignInProvider } from './types';
import {
IdentityProviders,
SignInProvider,
SignInProviderConfig,
} from './types';
import { commonProvider } from './commonProvider';
import { guestProvider } from './guestProvider';
import { customProvider } from './customProvider';
@@ -33,7 +37,7 @@ export type SignInProviderType = {
[key: string]: {
components: SignInProvider;
id: string;
config?: SignInConfig;
config?: SignInProviderConfig;
};
};
@@ -62,7 +66,7 @@ export function getSignInProviders(
return acc;
}
const { id } = config as SignInConfig;
const { id } = config as SignInProviderConfig;
validateIDs(id, acc);
acc[id] = { components: signInProviders.common, id, config };
+3 -3
View File
@@ -25,17 +25,17 @@ import {
SessionApi,
} from '@backstage/core-api';
export type SignInConfig = {
export type SignInProviderConfig = {
id: string;
title: string;
message: string;
apiRef: ApiRef<ProfileInfoApi & BackstageIdentityApi & SessionApi>;
};
export type IdentityProviders = ('guest' | 'custom' | SignInConfig)[];
export type IdentityProviders = ('guest' | 'custom' | SignInProviderConfig)[];
export type ProviderComponent = ComponentType<
SignInPageProps & { config: SignInConfig }
SignInPageProps & { config: SignInProviderConfig }
>;
export type ProviderLoader = (
+123
View File
@@ -1,5 +1,128 @@
# @backstage/create-app
## 0.3.18
### Patch Changes
- b49a525ab: Fixing dependency resolution for problematic library `graphql-language-service-interface`.
This change might not have to be applied to your local installation, however if you run into this error:
```
Error: Failed to compile.
/tmp/backstage-e2e-uMeycm/test-app/node_modules/graphql-language-service-interface/esm/GraphQLLanguageService.js 100:23
Module parse failed: Unexpected token (100:23)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| }
| let customRules = null;
> if (extensions?.customValidationRules &&
| typeof extensions.customValidationRules === 'function') {
| customRules = extensions.customValidationRules(this._graphQLConfig);
```
You can fix it by adding the following to the root `package.json`.
```json
...
"resolutions": {
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
},
...
```
- a360f9478: Expose the catalog-import route as an external route from the scaffolder.
This will make it possible to hide the "Register Existing Component" button
when you for example are running backstage with `catalog.readonly=true`.
As a consequence of this change you need add a new binding to your createApp call to
keep the button visible. However, if you instead want to hide the button you can safely
ignore the following example.
To bind the external route from the catalog-import plugin to the scaffolder template
index page, make sure you have the appropriate imports and add the following
to the createApp call:
```typescript
import { catalogImportPlugin } from '@backstage/plugin-catalog-import';
const app = createApp({
// ...
bindRoutes({ bind }) {
// ...
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
});
},
});
```
- f1952337c: Due to a change in the techdocs publishers, they don't check if they are able to reach e.g. the configured S3 bucket anymore.
This can be added again by the following change. Note that the backend process will no longer exit when it is not reachable but will only emit an error log message.
You should include the check when your backend to get early feedback about a potential misconfiguration:
```diff
// packages/backend/src/plugins/techdocs.ts
export default async function createPlugin({
logger,
config,
discovery,
reader,
}: PluginEnvironment): Promise<Router> {
// ...
const publisher = await Publisher.fromConfig(config, {
logger,
discovery,
})
+ // checks if the publisher is working and logs the result
+ await publisher.getReadiness();
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
// ...
}
```
- Updated dependencies [d8ffec739]
- Updated dependencies [7abec4dbc]
- Updated dependencies [017192ee8]
- Updated dependencies [a360f9478]
- Updated dependencies [bb5055aee]
- Updated dependencies [d840d30bc]
- Updated dependencies [d0d1c2f7b]
- Updated dependencies [5d0740563]
- Updated dependencies [b25846562]
- Updated dependencies [12390778e]
- Updated dependencies [cba5944fc]
- Updated dependencies [a376e3ee8]
- Updated dependencies [fef852ecd]
- Updated dependencies [18f7345a6]
- Updated dependencies [5cafcf452]
- Updated dependencies [423a514c3]
- Updated dependencies [86a95ba67]
- Updated dependencies [442f34b87]
- Updated dependencies [e27cb6c45]
- Updated dependencies [184b02bef]
- Updated dependencies [0b7fd7a9d]
- Updated dependencies [60ce64aa2]
- @backstage/plugin-scaffolder-backend@0.9.6
- @backstage/plugin-catalog-backend@0.7.1
- @backstage/plugin-scaffolder@0.9.0
- @backstage/catalog-model@0.7.7
- @backstage/core@0.7.5
- @backstage/plugin-catalog@0.5.4
- @backstage/plugin-api-docs@0.4.11
- @backstage/plugin-techdocs-backend@0.7.1
- @backstage/plugin-techdocs@0.7.2
- @backstage/catalog-client@0.3.10
- @backstage/plugin-tech-radar@0.3.9
- @backstage/cli@0.6.8
## 0.3.17
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.3.17",
"version": "0.3.18",
"private": false,
"publishConfig": {
"access": "public"
@@ -25,14 +25,10 @@ backend:
database:
client: pg
connection:
host:
$env: POSTGRES_HOST
port:
$env: POSTGRES_PORT
user:
$env: POSTGRES_USER
password:
$env: POSTGRES_PASSWORD
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
# https://node-postgres.com/features/ssl
#ssl: require # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require)
#ca: # if you have a CA file and want to verify it you can uncomment this section
@@ -43,13 +39,11 @@ backend:
integrations:
github:
- host: github.com
token:
$env: GITHUB_TOKEN
token: ${GITHUB_TOKEN}
### Example for how to add your GitHub Enterprise instance using the API:
# - host: ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token:
# $env: GHE_TOKEN
# token: ${GHE_TOKEN}
proxy:
'/test':
@@ -73,8 +67,7 @@ auth:
scaffolder:
github:
token:
$env: GITHUB_TOKEN
token: ${GITHUB_TOKEN}
visibility: public # or 'internal' or 'private'
catalog:
@@ -34,6 +34,9 @@ export default async function createPlugin({
discovery,
});
// checks if the publisher is working and logs the result
await publisher.getReadiness();
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
+54
View File
@@ -1,5 +1,59 @@
# @backstage/techdocs-common
## 0.5.0
### Minor Changes
- bc9d62f4f: Move the sanity checks of the publisher configurations to a dedicated `PublisherBase#getReadiness()` method instead of throwing an error when doing `Publisher.fromConfig(...)`.
You should include the check when your backend to get early feedback about a potential misconfiguration:
```diff
// packages/backend/src/plugins/techdocs.ts
export default async function createPlugin({
logger,
config,
discovery,
reader,
}: PluginEnvironment): Promise<Router> {
// ...
const publisher = await Publisher.fromConfig(config, {
logger,
discovery,
})
+ // checks if the publisher is working and logs the result
+ await publisher.getReadiness();
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
// ...
}
```
If you want to crash your application on invalid configurations, you can throw an `Error` to preserve the old behavior.
Please be aware that this is not the recommended for the use in a Backstage backend but might be helpful in CLI tools such as the `techdocs-cli`.
```ts
const publisher = await Publisher.fromConfig(config, {
logger,
discovery,
});
const ready = await publisher.getReadiness();
if (!ready.isAvailable) {
throw new Error('Invalid TechDocs publisher configuration');
}
```
### Patch Changes
- Updated dependencies [bb5055aee]
- Updated dependencies [5d0740563]
- @backstage/catalog-model@0.7.7
## 0.4.5
### Patch Changes
@@ -81,10 +81,12 @@ class Bucket {
this.bucketName = bucketName;
}
getMetadata() {
return new Promise(resolve => {
resolve('');
});
async getMetadata() {
if (this.bucketName === 'errorBucket') {
throw Error('Bucket does not exist');
}
return '';
}
upload(source: string, { destination }) {
+10 -4
View File
@@ -80,10 +80,16 @@ export class S3 {
};
}
headBucket() {
return new Promise(resolve => {
resolve('');
});
headBucket({ Bucket }) {
return {
promise: async () => {
if (Bucket === 'errorBucket') {
throw new Error('Bucket does not exist');
}
return {};
},
};
}
upload({ Key }: { Key: string }) {
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from 'events';
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
import { EventEmitter } from 'events';
import { ClientError } from 'pkgcloud';
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
@@ -37,12 +38,11 @@ class PkgCloudStorageClient {
getFile(
containerName: string,
file: string,
callback: (err: any, file: string) => any,
callback: (err: any, file: any) => any,
) {
checkFileExists(file).then(res => {
if (!res) {
callback('File does not exist', file);
throw new Error('File does not exist');
callback('File does not exist', undefined);
} else {
callback(undefined, 'success');
}
@@ -51,13 +51,12 @@ class PkgCloudStorageClient {
getContainer(
containerName: string,
callback: (err: string, container: string) => any,
callback: (err: ClientError, container: any) => any,
) {
if (containerName !== 'mock') {
callback('Container does not exist', containerName);
throw new Error('Container does not exist');
callback(new Error('Container does not exist'), undefined);
} else {
callback('Container does not exist', 'success');
callback(undefined, 'success');
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/techdocs-common",
"description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli",
"version": "0.4.5",
"version": "0.5.0",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -39,7 +39,7 @@
"@azure/identity": "^1.2.2",
"@azure/storage-blob": "^12.4.0",
"@backstage/backend-common": "^0.6.0",
"@backstage/catalog-model": "^0.7.4",
"@backstage/catalog-model": "^0.7.7",
"@backstage/config": "^0.1.4",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
@@ -62,7 +62,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.6.5",
"@backstage/cli": "^0.6.8",
"@types/fs-extra": "^9.0.5",
"@types/git-url-parse": "^9.0.0",
"@types/js-yaml": "^4.0.0",
@@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import {
Entity,
EntityName,
ENTITY_DEFAULT_NAMESPACE,
EntityName,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import mockFs from 'mock-fs';
import os from 'os';
import path from 'path';
import * as winston from 'winston';
import { AwsS3Publish } from './awsS3';
import { PublisherBase, TechDocsMetadata } from './types';
@@ -59,9 +59,7 @@ const getEntityRootDir = (entity: Entity) => {
return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name);
};
const logger = winston.createLogger();
jest.spyOn(logger, 'info').mockReturnValue(logger);
jest.spyOn(logger, 'error').mockReturnValue(logger);
const logger = getVoidLogger();
let publisher: PublisherBase;
@@ -87,6 +85,39 @@ beforeEach(() => {
});
describe('AwsS3Publish', () => {
describe('getReadiness', () => {
it('should validate correct config', async () => {
expect(await publisher.getReadiness()).toEqual({
isAvailable: true,
});
});
it('should reject incorrect config', async () => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
publisher: {
type: 'awsS3',
awsS3: {
credentials: {
accessKeyId: 'accessKeyId',
secretAccessKey: 'secretAccessKey',
},
// this bucket name will throw an error
bucketName: 'errorBucket',
},
},
},
});
const errorPublisher = AwsS3Publish.fromConfig(mockConfig, logger);
expect(await errorPublisher.getReadiness()).toEqual({
isAvailable: false,
});
});
});
describe('publish', () => {
beforeEach(() => {
const entity = createMockEntity();
@@ -17,16 +17,21 @@ import { Entity, EntityName } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import aws, { Credentials } from 'aws-sdk';
import { ManagedUpload } from 'aws-sdk/clients/s3';
import { CredentialsOptions } from 'aws-sdk/lib/credentials';
import express from 'express';
import fs from 'fs-extra';
import JSON5 from 'json5';
import createLimiter from 'p-limit';
import { CredentialsOptions } from 'aws-sdk/lib/credentials';
import path from 'path';
import { Readable } from 'stream';
import { Logger } from 'winston';
import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers';
import { PublisherBase, PublishRequest, TechDocsMetadata } from './types';
import {
PublisherBase,
PublishRequest,
ReadinessResponse,
TechDocsMetadata,
} from './types';
const streamToBuffer = (stream: Readable): Promise<Buffer> => {
return new Promise((resolve, reject) => {
@@ -81,30 +86,6 @@ export class AwsS3Publish implements PublisherBase {
...(endpoint && { endpoint }),
});
// Check if the defined bucket exists. Being able to connect means the configuration is good
// and the storage client will work.
storageClient.headBucket(
{
Bucket: bucketName,
},
err => {
if (err) {
logger.error(
`Could not retrieve metadata about the AWS S3 bucket ${bucketName}. ` +
'Make sure the bucket exists. Also make sure that authentication is setup either by ' +
'explicitly defining credentials and region in techdocs.publisher.awsS3 in app config or ' +
'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
);
logger.error(`from AWS client library: ${err.message}`);
throw new Error();
} else {
logger.info(
`Successfully connected to the AWS S3 bucket ${bucketName}.`,
);
}
},
);
return new AwsS3Publish(storageClient, bucketName, logger);
}
@@ -149,6 +130,35 @@ export class AwsS3Publish implements PublisherBase {
this.logger = logger;
}
/**
* Check if the defined bucket exists. Being able to connect means the configuration is good
* and the storage client will work.
*/
async getReadiness(): Promise<ReadinessResponse> {
try {
await this.storageClient
.headBucket({ Bucket: this.bucketName })
.promise();
this.logger.info(
`Successfully connected to the AWS S3 bucket ${this.bucketName}.`,
);
return { isAvailable: true };
} catch (error) {
this.logger.error(
`Could not retrieve metadata about the AWS S3 bucket ${this.bucketName}. ` +
'Make sure the bucket exists. Also make sure that authentication is setup either by ' +
'explicitly defining credentials and region in techdocs.publisher.awsS3 in app config or ' +
'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
);
this.logger.error(`from AWS client library`, error);
return {
isAvailable: false,
};
}
}
/**
* Upload all the files from the generated `directory` to the S3 bucket.
* Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html
@@ -16,8 +16,8 @@
import { getVoidLogger } from '@backstage/backend-common';
import {
Entity,
EntityName,
ENTITY_DEFAULT_NAMESPACE,
EntityName,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import mockFs from 'mock-fs';
@@ -59,12 +59,9 @@ const getEntityRootDir = (entity: Entity) => {
return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name);
};
function createLogger() {
const logger = getVoidLogger();
jest.spyOn(logger, 'info').mockReturnValue(logger);
jest.spyOn(logger, 'error').mockReturnValue(logger);
return logger;
}
const logger = getVoidLogger();
jest.spyOn(logger, 'info').mockReturnValue(logger);
jest.spyOn(logger, 'error').mockReturnValue(logger);
let publisher: PublisherBase;
beforeEach(async () => {
@@ -85,13 +82,51 @@ beforeEach(async () => {
},
});
publisher = await AzureBlobStoragePublish.fromConfig(
mockConfig,
createLogger(),
);
publisher = AzureBlobStoragePublish.fromConfig(mockConfig, logger);
});
describe('publishing with valid credentials', () => {
describe('getReadiness', () => {
it('should validate correct config', async () => {
expect(await publisher.getReadiness()).toEqual({
isAvailable: true,
});
});
it('should reject incorrect config', async () => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
publisher: {
type: 'azureBlobStorage',
azureBlobStorage: {
credentials: {
accountName: 'accountName',
accountKey: 'accountKey',
},
containerName: 'bad_container',
},
},
},
});
const errorPublisher = await AzureBlobStoragePublish.fromConfig(
mockConfig,
logger,
);
expect(await errorPublisher.getReadiness()).toEqual({
isAvailable: false,
});
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(
`Could not retrieve metadata about the Azure Blob Storage container bad_container.`,
),
);
});
});
describe('publish', () => {
beforeEach(() => {
const entity = createMockEntity();
@@ -151,6 +186,60 @@ describe('publishing with valid credentials', () => {
});
mockFs.restore();
});
it('reports an error when bad account credentials', async () => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
publisher: {
type: 'azureBlobStorage',
azureBlobStorage: {
credentials: {
accountName: 'failupload',
accountKey: 'accountKey',
},
containerName: 'containerName',
},
},
},
});
publisher = await AzureBlobStoragePublish.fromConfig(mockConfig, logger);
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
mockFs({
[entityRootDir]: {
'index.html': '',
},
});
let error;
try {
await publisher.publish({
entity,
directory: entityRootDir,
});
} catch (e) {
error = e;
}
expect(error.message).toContain(
`Unable to upload file(s) to Azure Blob Storage.`,
);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(
`Unable to upload file(s) to Azure Blob Storage. Error: Upload failed for ${path.join(
entityRootDir,
'index.html',
)} with status code 500`,
),
);
mockFs.restore();
});
});
describe('hasDocsBeenGenerated', () => {
@@ -243,156 +332,3 @@ describe('publishing with valid credentials', () => {
});
});
});
describe('error reporting', () => {
it('reports an error when unable to read container properties', async () => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
publisher: {
type: 'azureBlobStorage',
azureBlobStorage: {
credentials: {
accountName: 'accountName',
},
containerName: 'bad_container',
},
},
},
});
const logger = createLogger();
let error;
try {
publisher = await AzureBlobStoragePublish.fromConfig(mockConfig, logger);
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(Error);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(
`Could not retrieve metadata about the Azure Blob Storage container bad_container.`,
),
);
});
it('reports an error when bad account credentials', async () => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
publisher: {
type: 'azureBlobStorage',
azureBlobStorage: {
credentials: {
accountName: 'failupload',
accountKey: 'accountKey',
},
containerName: 'containerName',
},
},
},
});
const logger = createLogger();
publisher = await AzureBlobStoragePublish.fromConfig(mockConfig, logger);
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
mockFs({
[entityRootDir]: {
'index.html': '',
},
});
let error;
try {
await publisher.publish({
entity,
directory: entityRootDir,
});
} catch (e) {
error = e;
}
expect(error.message).toContain(
`Unable to upload file(s) to Azure Blob Storage.`,
);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(
`Unable to upload file(s) to Azure Blob Storage. Error: Upload failed for ${path.join(
entityRootDir,
'index.html',
)} with status code 500`,
),
);
mockFs.restore();
});
describe('fetchTechDocsMetadata', () => {
it('should return tech docs metadata', async () => {
const entityNameMock = createMockEntityName();
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
mockFs({
[entityRootDir]: {
'techdocs_metadata.json':
'{"site_name": "backstage", "site_description": "site_content", "etag": "etag"}',
},
});
const expectedMetadata: TechDocsMetadata = {
site_name: 'backstage',
site_description: 'site_content',
etag: 'etag',
};
expect(
await publisher.fetchTechDocsMetadata(entityNameMock),
).toStrictEqual(expectedMetadata);
mockFs.restore();
});
it('should return tech docs metadata when json encoded with single quotes', async () => {
const entityNameMock = createMockEntityName();
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
mockFs({
[entityRootDir]: {
'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag'}`,
},
});
const expectedMetadata: TechDocsMetadata = {
site_name: 'backstage',
site_description: 'site_content',
etag: 'etag',
};
expect(
await publisher.fetchTechDocsMetadata(entityNameMock),
).toStrictEqual(expectedMetadata);
mockFs.restore();
});
it('should return an error if the techdocs_metadata.json file is not present', async () => {
const entityNameMock = createMockEntityName();
let error;
try {
await publisher.fetchTechDocsMetadata(entityNameMock);
} catch (e) {
error = e;
}
expect(error.message).toEqual(
expect.stringContaining('TechDocs metadata fetch'),
);
});
});
});
@@ -26,16 +26,18 @@ import limiterFactory from 'p-limit';
import { default as path, default as platformPath } from 'path';
import { Logger } from 'winston';
import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers';
import { PublisherBase, PublishRequest, TechDocsMetadata } from './types';
import {
PublisherBase,
PublishRequest,
ReadinessResponse,
TechDocsMetadata,
} from './types';
// The number of batches that may be ongoing at the same time.
const BATCH_CONCURRENCY = 3;
export class AzureBlobStoragePublish implements PublisherBase {
static async fromConfig(
config: Config,
logger: Logger,
): Promise<PublisherBase> {
static fromConfig(config: Config, logger: Logger): PublisherBase {
let containerName = '';
try {
containerName = config.getString(
@@ -78,26 +80,6 @@ export class AzureBlobStoragePublish implements PublisherBase {
credential,
);
try {
const response = await storageClient
.getContainerClient(containerName)
.getProperties();
if (response._response.status >= 400) {
throw new Error(
`Failed to retrieve metadata from ${response._response.request.url} with status code ${response._response.status}.`,
);
}
} catch (e) {
logger.error(
`Could not retrieve metadata about the Azure Blob Storage container ${containerName}. ` +
'Make sure that the Azure project and container exist and the access key is setup correctly ' +
'techdocs.publisher.azureBlobStorage.credentials defined in app config has correct permissions. ' +
'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
);
throw new Error(`from Azure Blob Storage client library: ${e.message}`);
}
return new AzureBlobStoragePublish(storageClient, containerName, logger);
}
@@ -111,6 +93,37 @@ export class AzureBlobStoragePublish implements PublisherBase {
this.logger = logger;
}
async getReadiness(): Promise<ReadinessResponse> {
try {
const response = await this.storageClient
.getContainerClient(this.containerName)
.getProperties();
if (response._response.status === 200) {
return {
isAvailable: true,
};
}
if (response._response.status >= 400) {
this.logger.error(
`Failed to retrieve metadata from ${response._response.request.url} with status code ${response._response.status}.`,
);
}
} catch (e) {
this.logger.error(`from Azure Blob Storage client library: ${e.message}`);
}
this.logger.error(
`Could not retrieve metadata about the Azure Blob Storage container ${this.containerName}. ` +
'Make sure that the Azure project and container exist and the access key is setup correctly ' +
'techdocs.publisher.azureBlobStorage.credentials defined in app config has correct permissions. ' +
'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
);
return { isAvailable: false };
}
/**
* Upload all the files from the generated `directory` to the Azure Blob Storage container.
* Directory structure used in the container is - entityNamespace/entityKind/entityName/index.html
@@ -16,8 +16,8 @@
import { getVoidLogger } from '@backstage/backend-common';
import {
Entity,
EntityName,
ENTITY_DEFAULT_NAMESPACE,
EntityName,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import mockFs from 'mock-fs';
@@ -83,6 +83,35 @@ beforeEach(async () => {
});
describe('GoogleGCSPublish', () => {
describe('getReadiness', () => {
it('should validate correct config', async () => {
expect(await publisher.getReadiness()).toEqual({
isAvailable: true,
});
});
it('should reject incorrect config', async () => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
publisher: {
type: 'googleGcs',
googleGcs: {
credentials: '{}',
bucketName: 'errorBucket',
},
},
},
});
const errorPublisher = GoogleGCSPublish.fromConfig(mockConfig, logger);
expect(await errorPublisher.getReadiness()).toEqual({
isAvailable: false,
});
});
});
describe('publish', () => {
beforeEach(() => {
const entity = createMockEntity();
@@ -26,13 +26,15 @@ import createLimiter from 'p-limit';
import path from 'path';
import { Logger } from 'winston';
import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers';
import { PublisherBase, PublishRequest, TechDocsMetadata } from './types';
import {
PublisherBase,
PublishRequest,
ReadinessResponse,
TechDocsMetadata,
} from './types';
export class GoogleGCSPublish implements PublisherBase {
static async fromConfig(
config: Config,
logger: Logger,
): Promise<PublisherBase> {
static fromConfig(config: Config, logger: Logger): PublisherBase {
let bucketName = '';
try {
bucketName = config.getString('techdocs.publisher.googleGcs.bucketName');
@@ -65,21 +67,6 @@ export class GoogleGCSPublish implements PublisherBase {
}),
});
// Check if the defined bucket exists. Being able to connect means the configuration is good
// and the storage client will work.
try {
await storageClient.bucket(bucketName).getMetadata();
logger.info(`Successfully connected to the GCS bucket ${bucketName}.`);
} catch (err) {
logger.error(
`Could not retrieve metadata about the GCS bucket ${bucketName}. ` +
'Make sure the bucket exists. Also make sure that authentication is setup either by explicitly defining ' +
'techdocs.publisher.googleGcs.credentials in app config or by using environment variables. ' +
'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
);
throw new Error(err.message);
}
return new GoogleGCSPublish(storageClient, bucketName, logger);
}
@@ -93,6 +80,33 @@ export class GoogleGCSPublish implements PublisherBase {
this.logger = logger;
}
/**
* Check if the defined bucket exists. Being able to connect means the configuration is good
* and the storage client will work.
*/
async getReadiness(): Promise<ReadinessResponse> {
try {
await this.storageClient.bucket(this.bucketName).getMetadata();
this.logger.info(
`Successfully connected to the GCS bucket ${this.bucketName}.`,
);
return {
isAvailable: true,
};
} catch (err) {
this.logger.error(
`Could not retrieve metadata about the GCS bucket ${this.bucketName}. ` +
'Make sure the bucket exists. Also make sure that authentication is setup either by explicitly defining ' +
'techdocs.publisher.googleGcs.credentials in app config or by using environment variables. ' +
'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
);
this.logger.error(`from GCS client library: ${err.message}`);
return { isAvailable: false };
}
}
/**
* Upload all the files from the generated `directory` to the GCS bucket.
* Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html
@@ -28,6 +28,7 @@ import {
PublisherBase,
PublishRequest,
PublishResponse,
ReadinessResponse,
TechDocsMetadata,
} from './types';
@@ -65,6 +66,12 @@ export class LocalPublish implements PublisherBase {
this.discovery = discovery;
}
async getReadiness(): Promise<ReadinessResponse> {
return {
isAvailable: true,
};
}
publish({ entity, directory }: PublishRequest): Promise<PublishResponse> {
const entityNamespace = entity.metadata.namespace ?? 'default';
@@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import {
Entity,
EntityName,
ENTITY_DEFAULT_NAMESPACE,
EntityName,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import mockFs from 'mock-fs';
import os from 'os';
import path from 'path';
import * as winston from 'winston';
import { OpenStackSwiftPublish } from './openStackSwift';
import { PublisherBase, TechDocsMetadata } from './types';
@@ -59,9 +59,7 @@ const getEntityRootDir = (entity: Entity) => {
return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name);
};
const logger = winston.createLogger();
jest.spyOn(logger, 'info').mockReturnValue(logger);
jest.spyOn(logger, 'error').mockReturnValue(logger);
const logger = getVoidLogger();
let publisher: PublisherBase;
@@ -89,6 +87,43 @@ beforeEach(() => {
});
describe('OpenStackSwiftPublish', () => {
describe('getReadiness', () => {
it('should validate correct config', async () => {
expect(await publisher.getReadiness()).toEqual({
isAvailable: true,
});
});
it('should reject incorrect config', async () => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
publisher: {
type: 'openStackSwift',
openStackSwift: {
credentials: {
username: 'mockuser',
password: 'verystrongpass',
},
authUrl: 'mockauthurl',
region: 'mockregion',
containerName: 'errorBucket',
},
},
},
});
const errorPublisher = OpenStackSwiftPublish.fromConfig(
mockConfig,
logger,
);
expect(await errorPublisher.getReadiness()).toEqual({
isAvailable: false,
});
});
});
describe('publish', () => {
beforeEach(() => {
const entity = createMockEntity();
@@ -15,16 +15,21 @@
*/
import { Entity, EntityName } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { storage } from 'pkgcloud';
import express from 'express';
import fs from 'fs-extra';
import JSON5 from 'json5';
import createLimiter from 'p-limit';
import path from 'path';
import { storage } from 'pkgcloud';
import { Readable } from 'stream';
import { Logger } from 'winston';
import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers';
import { PublisherBase, PublishRequest, TechDocsMetadata } from './types';
import {
PublisherBase,
PublishRequest,
ReadinessResponse,
TechDocsMetadata,
} from './types';
const streamToBuffer = (stream: Readable): Promise<Buffer> => {
return new Promise((resolve, reject) => {
@@ -70,25 +75,6 @@ export class OpenStackSwiftPublish implements PublisherBase {
region: openStackSwiftConfig.getString('region'),
});
// Check if the defined container exists. Being able to connect means the configuration is good
// and the storage client will work.
storageClient.getContainer(containerName, (err, container) => {
if (container) {
logger.info(
`Successfully connected to the OpenStack Swift container ${containerName}.`,
);
} else {
logger.error(
`Could not retrieve metadata about the OpenStack Swift container ${containerName}. ` +
'Make sure the container exists. Also make sure that authentication is setup either by ' +
'explicitly defining credentials and region in techdocs.publisher.openStackSwift in app config or ' +
'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
);
logger.error(`from OpenStack client library: ${err.message}`);
}
});
return new OpenStackSwiftPublish(storageClient, containerName, logger);
}
@@ -102,6 +88,37 @@ export class OpenStackSwiftPublish implements PublisherBase {
this.logger = logger;
}
/*
* Check if the defined container exists. Being able to connect means the configuration is good
* and the storage client will work.
*/
getReadiness(): Promise<ReadinessResponse> {
return new Promise(resolve => {
this.storageClient.getContainer(this.containerName, (err, container) => {
if (container) {
this.logger.info(
`Successfully connected to the OpenStack Swift container ${this.containerName}.`,
);
resolve({
isAvailable: true,
});
} else {
this.logger.error(
`Could not retrieve metadata about the OpenStack Swift container ${this.containerName}. ` +
'Make sure the container exists. Also make sure that authentication is setup either by ' +
'explicitly defining credentials and region in techdocs.publisher.openStackSwift in app config or ' +
'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
);
this.logger.error(`from OpenStack client library: ${err.message}`);
resolve({
isAvailable: false,
});
}
});
});
}
/**
* Upload all the files from the generated `directory` to the OpenStack Swift container.
* Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html
@@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { PublisherType, PublisherBase } from './types';
import { LocalPublish } from './local';
import { GoogleGCSPublish } from './googleStorage';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import { Logger } from 'winston';
import { AwsS3Publish } from './awsS3';
import { AzureBlobStoragePublish } from './azureBlobStorage';
import { GoogleGCSPublish } from './googleStorage';
import { LocalPublish } from './local';
import { OpenStackSwiftPublish } from './openStackSwift';
import { PublisherBase, PublisherType } from './types';
type factoryOptions = {
logger: Logger;
@@ -45,7 +45,7 @@ export class Publisher {
switch (publisherType) {
case 'googleGcs':
logger.info('Creating Google Storage Bucket publisher for TechDocs');
return await GoogleGCSPublish.fromConfig(config, logger);
return GoogleGCSPublish.fromConfig(config, logger);
case 'awsS3':
logger.info('Creating AWS S3 Bucket publisher for TechDocs');
return AwsS3Publish.fromConfig(config, logger);
@@ -37,6 +37,14 @@ export type PublishResponse = {
remoteUrl?: string;
} | void;
/**
* Result for the validation check.
*/
export type ReadinessResponse = {
/** If true, the publisher is able to interact with the backing storage. */
isAvailable: boolean;
};
/**
* Type to hold metadata found in techdocs_metadata.json and associated with each site
* @param etag ETag of the resource used to generate the site. Usually the latest commit sha of the source repository.
@@ -53,6 +61,14 @@ export type TechDocsMetadata = {
* It also provides APIs to communicate with the storage service.
*/
export interface PublisherBase {
/**
* Check if the publisher is ready. This check tries to perform certain checks to see if the
* publisher is configured correctly and can be used to publish or read documentations.
* The different implementations might e.g. use the provided service credentials to access the
* target or check if a folder/bucket is available.
*/
getReadiness(): Promise<ReadinessResponse>;
/**
* Store the generated static files onto a storage service (either local filesystem or external service).
*
+1 -1
View File
@@ -63,5 +63,5 @@ export const pageTheme: Record<string, PageTheme> = {
library: genPageTheme(colorVariants.rubyRed, shapes.wave),
other: genPageTheme(colorVariants.darkGrey, shapes.wave),
app: genPageTheme(colorVariants.toastyOrange, shapes.wave),
apis: genPageTheme(colorVariants.eveningSea, shapes.wave2),
apis: genPageTheme(colorVariants.teal, shapes.wave2),
};
+15
View File
@@ -1,5 +1,20 @@
# @backstage/plugin-api-docs
## 0.4.11
### Patch Changes
- 12390778e: chore(deps): bump @asyncapi/react-component from 0.19.2 to 0.22.3
- 5cafcf452: add debounce time attribute for apis-docs for search, giving more time to the users when they are typing.
- Updated dependencies [bb5055aee]
- Updated dependencies [d0d1c2f7b]
- Updated dependencies [5d0740563]
- Updated dependencies [5cafcf452]
- Updated dependencies [86a95ba67]
- Updated dependencies [e27cb6c45]
- @backstage/catalog-model@0.7.7
- @backstage/core@0.7.5
## 0.4.10
### Patch Changes
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
"version": "0.4.10",
"version": "0.4.11",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@asyncapi/react-component": "^0.19.2",
"@backstage/catalog-model": "^0.7.5",
"@backstage/core": "^0.7.4",
"@asyncapi/react-component": "^0.22.3",
"@backstage/catalog-model": "^0.7.7",
"@backstage/core": "^0.7.5",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/theme": "^0.2.5",
"@material-icons/font": "^1.0.2",
@@ -49,7 +49,7 @@
"swagger-ui-react": "^3.37.2"
},
"devDependencies": {
"@backstage/cli": "^0.6.7",
"@backstage/cli": "^0.6.8",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
+2 -2
View File
@@ -21,7 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.6",
"@backstage/core": "^0.7.4",
"@backstage/core": "^0.7.5",
"@backstage/errors": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.3",
"@backstage/theme": "^0.2.5",
@@ -34,7 +34,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.6.7",
"@backstage/cli": "^0.6.8",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
+1 -2
View File
@@ -55,8 +55,7 @@ proxy:
target: 'https://api.bitrise.io/v0.1'
allowedMethods: ['GET']
headers:
Authorization:
$env: BITRISE_AUTH_TOKEN
Authorization: ${BITRISE_AUTH_TOKEN}
```
Learn on https://devcenter.bitrise.io/api/authentication how to create a new Bitrise token.
+2 -2
View File
@@ -21,7 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.2",
"@backstage/core": "^0.7.4",
"@backstage/core": "^0.7.5",
"@backstage/plugin-catalog-react": "^0.1.2",
"@backstage/theme": "^0.2.5",
"@material-ui/core": "^4.11.0",
@@ -37,7 +37,7 @@
"recharts": "^1.8.5"
},
"devDependencies": {
"@backstage/cli": "^0.6.7",
"@backstage/cli": "^0.6.8",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
+17
View File
@@ -1,5 +1,22 @@
# @backstage/plugin-catalog-backend
## 0.7.1
### Patch Changes
- 017192ee8: Add support for configure an LDAP query filter on multiple lines.
- 5d0740563: Implemented missing support for the dependsOn/dependencyOf relationships
between `Component` and `Resource` catalog model objects.
Added support for generating the relevant relationships to the
`BuiltinKindsEntityProcessor`, and added simple support for fetching
relationships between `Components` and `Resources` for rendering in the
system diagram. All catalog-model changes backwards compatible.
- Updated dependencies [bb5055aee]
- Updated dependencies [5d0740563]
- @backstage/catalog-model@0.7.7
## 0.7.0
### Minor Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
"version": "0.7.0",
"version": "0.7.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,7 +31,7 @@
"dependencies": {
"@azure/msal-node": "^1.0.0-beta.3",
"@backstage/backend-common": "^0.6.1",
"@backstage/catalog-model": "^0.7.5",
"@backstage/catalog-model": "^0.7.7",
"@backstage/config": "^0.1.4",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
@@ -62,7 +62,7 @@
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.6.6",
"@backstage/cli": "^0.6.8",
"@backstage/test-utils": "^0.1.9",
"@types/core-js": "^2.5.4",
"@types/git-url-parse": "^9.0.0",
+2 -2
View File
@@ -32,7 +32,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.7.6",
"@backstage/catalog-client": "^0.3.9",
"@backstage/core": "^0.7.4",
"@backstage/core": "^0.7.5",
"@backstage/integration": "^0.5.0",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.4",
@@ -53,7 +53,7 @@
"yaml": "^1.10.0"
},
"devDependencies": {
"@backstage/cli": "^0.6.7",
"@backstage/cli": "^0.6.8",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
+23
View File
@@ -1,5 +1,28 @@
# @backstage/plugin-catalog
## 0.5.4
### Patch Changes
- 5d0740563: Implemented missing support for the dependsOn/dependencyOf relationships
between `Component` and `Resource` catalog model objects.
Added support for generating the relevant relationships to the
`BuiltinKindsEntityProcessor`, and added simple support for fetching
relationships between `Components` and `Resources` for rendering in the
system diagram. All catalog-model changes backwards compatible.
- Updated dependencies [bb5055aee]
- Updated dependencies [d0d1c2f7b]
- Updated dependencies [5d0740563]
- Updated dependencies [5cafcf452]
- Updated dependencies [86a95ba67]
- Updated dependencies [442f34b87]
- Updated dependencies [e27cb6c45]
- @backstage/catalog-model@0.7.7
- @backstage/core@0.7.5
- @backstage/catalog-client@0.3.10
## 0.5.3
### Patch Changes

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