Merge branch 'master' into anderoo/cuatom-columns-on-deployments-plugin

This commit is contained in:
Andrew Johnson
2021-04-16 01:12:07 +01:00
committed by GitHub
316 changed files with 10157 additions and 2916 deletions
-15
View File
@@ -1,15 +0,0 @@
---
'@backstage/config-loader': minor
---
Fix bug where `$${...}` was not being escaped to `${...}`
Add support for environment variable substitution in `$include`, `$file` and
`$env` transform values.
- This change allows for including dynamic paths, such as environment specific
secrets by using the same environment variable substitution (`${..}`) already
supported outside of the various include transforms.
- If you are currently using the syntax `${...}` in your include transform values,
you will need to escape the substitution by using `$${...}` instead to maintain
the same behavior.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-auth-backend': patch
---
When using OAuth2 authentication the name is now taken from the name property of the JWT instead of the email property
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/core-api': patch
'@backstage/core': patch
---
Improved error messaging for routable extension errors, making it easier to identify the component and mount point that caused the error.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/catalog-model': patch
---
Added `stringifyEntityRef`, which always creates a string representation of an entity reference. Also deprecated `serializeEntityRef`, as `stringifyEntityRef` should be used instead.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
Exported SignInProviderConfig to strongly type SignInPage providers
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/core-api': patch
'@backstage/core': patch
---
Fixed a bug with `useRouteRef` where navigating from routes beneath a mount point would often fail.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/test-utils': patch
---
Remove unnecessary wrapping of elements rendered by `wrapInTestApp` and `renderInTestApp`, which was breaking mount discovery.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-org': patch
---
Optimize data fetched for the `OwnershipCard`.
+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
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-github-deployments': patch
---
Add a button to reload the GitHub Deployments card
+4
View File
@@ -9,9 +9,11 @@ Blackbox
Chai
Changesets
Chanwit
Cloudformation
Codecov
Codehilite
Config
Debounce
Discoverability
Dockerfile
Dockerize
@@ -110,7 +112,9 @@ cookiecutter
css
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
+1
View File
@@ -24,3 +24,4 @@
| [Trendyol](https://trendyol.com) | [Erdogan Oksuz](https://github.com/erdoganoksuz) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. |
| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. |
| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. |
| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a teams engineering dependencies. |
+58 -110
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,32 +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: ${GCS_PRIVATE_KEY}
catalog:
rules:
@@ -168,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:
@@ -198,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')
@@ -251,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:
@@ -282,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:
@@ -0,0 +1,170 @@
# Using AWS Application Load Balancer with Azure Active Directory to authenticate requests
Backstage allows offloading the responsibility of authenticating users to an AWS Application Load Balancer (**ALB**), leveraging the authentication support on ALB.
This tutorial shows how to use authentication on an ALB sitting in front of Backstage.
Azure Active Directory (**AAD**) is used as identity provider but any identity provider supporting OpenID Connect (OIDC) can be used.
It is assumed an ALB is already serving traffic in front of a Backstage instance configured to serve the frontend app from the backend.
## Infrastructure setup
### AAD App
The AAD App is used to execute the authentication flow, serve and refresh the identity token.
Create the AAD App following the steps outlined in `Create a Microsoft App Registration in Microsoft Portal` section from the tutorial [Monorepo App Setup With Authentication][monorepo-app-setup-with-auth].
Instead of `localhost` addresses, use the following values.
- Identifier URI: `https://backstage.yourdomain.com`
- Redirect URI: `https://backstage.yourdomain.com/oauth2/idpresponse`
`Application (client) Id`, `Directory (tenant) ID` and `client secret`values will be used while configuring the ALB.
### ALB
In the AWS console, configure ALB Authentication:
- Edit the ALB rule used to forward the traffic to Backstage and add a new `Authenticate` action. The action will have higher priority compared to the existing `Forward to`.
- Select `OIDC` under `Authenticate`
- Set `Issuer` to `https://login.microsoftonline.com/{TENANT_ID}/v2.0`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App.
- Set `Authorization endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App.
- Set `Token endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App.
- Set `User info endpoint` to `https://graph.microsoft.com/oidc/userinfo`
- Set `Client ID` to the AAD App `Application (client) Id`
- Set `Client secret` to the AAD APP `client secret`
Use the following advanced settings:
- `Session cookie name` = `AWSELBAuthSessionCookie`
- `Session timeout` = `604800` seconds
- `Scope` = `openid profile offline_access`
- `Action on unauthenticated request` = `Autenticate (client reattempt)`
Once you've saved the action, you should see an authentication flow be triggered against AAD when visiting Backstage address at `https://backstage.yourdomain.com`. The flow will not complete successfully as the Backstage app isn't yet configured properly.
## Backstage changes
### Frontend
The Backstage App needs a SignInPage when authentication is required.
When using ALB authentication Backstage will only be loaded once the user has successfully authenticated; we won't need to display a SignIn page, however we will need to create a dummy SignIn component that can refresh the token.
- edit `packages/app/src/App.tsx`
- import the following two additional definitions from `@backstage/core`: `useApi`, `configApiRef`; these will be used to check whether Backstage is running locally or behind an ALB
- add the following definition just before the app is created (`const app = createApp`):
```ts
const DummySignInComponent: any = (props: any) => {
try {
const config = useApi(configApiRef);
const shouldAuth = !!config.getOptionalConfig('auth.providers.awsalb');
if (shouldAuth) {
fetch(`${window.location.origin}/api/auth/awsalb/refresh`)
.then(data => data.json())
.then(data => {
props.onResult({
userId: data.backstageIdentity.id,
profile: data.profile,
});
});
} else {
props.onResult({
userId: 'guest',
profile: {
email: 'guest@example.com',
displayName: 'Guest',
picture: '',
},
});
}
return <div />;
} catch (err) {
return <div>{err.message}</div>;
}
};
```
- add `DummySignInComponent` as `SignInPage`:
```ts
const app = createApp({
...
components: {
SignInPage: DummySignInComponent,
...
},
...
});
```
### Backend
When using ALB auth it is not possible to leverage the built-in auth config discovery mechanism implemented in the app created by default; bespoke logic needs to be implemented.
- replace the content of `packages/backend/plugin/auth.ts` with the below
```ts
import {
createRouter,
AuthResponse,
AuthProviderFactoryOptions,
defaultAuthProviderFactories,
} from '@backstage/plugin-auth-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
database,
config,
discovery,
}: PluginEnvironment) {
const identityResolver = (payload: any): Promise<AuthResponse<any>> => {
return Promise.resolve({
providerInfo: {},
profile: {
email: payload.email,
displayName: payload.name,
picture: payload.picture,
},
backstageIdentity: {
id: payload.email,
},
});
};
const providerFactories = {
awsalb: (options: AuthProviderFactoryOptions) =>
defaultAuthProviderFactories.awsalb({ ...options, identityResolver }),
};
return await createRouter({
logger,
config,
database,
discovery,
providerFactories,
});
}
```
### Configuration
Use the following `auth` configuration when running Backstage on AWS:
```yaml
auth:
providers:
awsalb:
issuer:
issuer: https://login.microsoftonline.com/{TENANT_ID}/v2.0
region: { AWS_REGION }
```
Replace `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App and `{AWS_REGION}` with the AWS region identifier where the ALB is deployed (for example: `eu-central-1`).
## Conclusion
Once it's deployed, after going through the AAD authentication flow, Backstage should display the AAD user details.
<!-- links -->
[monorepo-app-setup-with-auth-ms]: https://backstage.io/docs/tutorials/quickstart-app-auth#the-auth-configuration
+195
View File
@@ -0,0 +1,195 @@
# Deploying Backstage on AWS using ECR and EKS
Backstage documentation shows how to build a [Docker
image](https://backstage.io/docs/getting-started/deployment-docker); this
tutorial shows how to deploy that Docker image to AWS using Elastic Container
Registry (ECR) and Elastic Kubernetes Service (EKS). Amazon also supports
deployments with Helm, covered in the [Helm
Kubernetes](../kubernetes/basic_kubernetes_example_with_helm) example.
The basic workflow for this method is to build a Backstage Docker image, upload
the new version to a container registry, and update a Kubernetes deployment with
the new image.
## Create a container registry
To create an Elastic Container Registry on AWS, go to the [AWS ECR
console](https://console.aws.amazon.com/ecr/repositories).
Click `Create repository` and give the repository a name, like `backstage`.
## Add an ECR IAM user
To push to this new ECR repository, there must be an IAM user with
`AmazonEC2ContainerRegistry` permission.
Go to [AWS IAM console](https://console.aws.amazon.com/iam/home) and select
`Users`.
1. Click `Add User`
2. Set the username to something like `ecr-publisher` and select `Programmatic access` only.
3. For `Permissions`, you can either create a new group or attach policies
directly to the user with `AmazonEC2ContainerRegistryFullAccess`.
4. Finish creating the user; an access key ID and secret access key will be
generated.
## Publish a Backstage build
Follow the [Docker
image](https://backstage.io/docs/getting-started/deployment-docker)
documentation to build a new Backstage Docker image:
```shell
$ yarn build
$ docker image build . -f packages/backend/Dockerfile --tag backstage
```
This command builds a backend-only image, but you can similarly build a frontend
or combined Docker image.
Next, configure the [AWS CLI](https://aws.amazon.com/cli/) to use the
`ecr-publisher` user you created:
```shell
$ aws configure
AWS Access Key ID: <access key ID>
AWS Secret Access Key: <secret access key>
Default region name: <region for your ECR repository>
```
Now you can use the AWS CLI to push the built image to the ECR repository. It's
a good practice to use a specific version tag as well as `latest` when pushing;
more about [Semver tagging
here](https://medium.com/@mccode/using-semantic-versioning-for-docker-image-tags-dfde8be06699).
Go to the [AWS ECR console](https://console.aws.amazon.com/ecr/repositories) and
click on your repository, then `View push commands`. This will show the
repository URL to push and some sample commands.
```shell
# Copy from push command window
$ aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <repo_url>
Login Succeeded
$ docker tag backstage:latest <repo_url>/backstage:latest
$ docker push <repo_url>/backstage:latest
$ docker tag backstage:latest <repo_url>/backstage:1.0.0
$ docker push <repo_url>/backstage:1.0.0
```
These will take a few minutes to upload the Docker images, then you can see
the image with `latest, 1.0.0` tags in the ECR repository. Now you can create a
Kubernetes deployment based on this image.
## Deploy to an EKS cluster
Creating an Elastic Kubernetes Service (EKS) cluster is beyond the scope of this
document, but it can be as easy as `eksctl create cluster` documented in the
[AWS EKS getting started
guide](https://docs.aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html),
which uses a Cloudformation template to create the necessary resources.
To deploy the Docker image to EKS, create a `kubernetes` folder in your
Backstage source folder and add a Kubernetes `deployment.yaml`:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage-backend
labels:
app: backstage-backend
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: backstage-backend
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: backstage-backend
spec:
containers:
- image: <repo_url>/backstage:1.0.0
imagePullPolicy: Always
name: backstage-backend
ports:
- containerPort: 7000
protocol: TCP
```
Note the `image` key in the container spec referencing the ECR repository.
Now create a simple `service.yaml` to map the container ports:
```yaml
apiVersion: v1
kind: Service
metadata:
name: backstage-backend
spec:
selector:
app: backstage-backend
ports:
- protocol: TCP
port: 80
targetPort: 7000
```
Apply these Kubernetes definitions to the EKS cluster to complete the Backstage
deployment:
```shell
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml
```
Now you can see your Backstage workload running from the [EKS
console](https://console.aws.amazon.com/eks/home).
## Further steps
### Exposing Backstage with a load balancer
Backstage users need to query the backend, which means we need to expose
the workload with a load balancer. Follow the [Application load balancing on
EKS](https://docs.aws.amazon.com/eks/latest/userguide/alb-ingress.html) guide to
set up a Load Balancer controller and Kubernetes ingress to your application.
This is ultimately a `kubectl apply` with an ingress definition:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: default
name: backstage-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backstage-backend
port:
number: 80
```
### Updating the deployment
To update the Kubernetes deployment to a newly published version of your
Backstage Docker image, update the image tag reference in `deployment.yaml` and
then apply the changes to EKS with `kubectl apply -f deployment.yaml`.
+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
+2 -26
View File
@@ -46,29 +46,5 @@ The Auth0 provider is a structure with three configuration keys:
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `auth0AuthApi` reference and
`SignInPage` component to `createApp` in `packages/app/src/App.tsx`:
```diff
+ import { auth0AuthApiRef, SignInConfig, SignInPage } from '@backstage/core';
+ const auth0Provider: SignInConfig = {
+ id: 'auth0-auth-provider',
+ title: 'Auth0',
+ message: 'Sign in using Auth0',
+ apiRef: auth0AuthApiRef,
+};
+
const app = createApp({
apis,
plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => (
+ <SignInPage
+ {...props}
+ auto
+ provider={auth0Provider}
+ />
+ ),
+ },
bindRoutes({ bind }) {
```
`SignInPage` component as shown in
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
+2 -26
View File
@@ -49,29 +49,5 @@ The GitHub provider is a structure with three configuration keys:
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `githubAuthApi` reference and
`SignInPage` component to `createApp` in `packages/app/src/App.tsx`:
```diff
+ import { githubAuthApiRef, SignInConfig, SignInPage } from '@backstage/core';
+ const githubProvider: SignInConfig = {
+ id: 'github-auth-provider',
+ title: 'GitHub',
+ message: 'Sign in using GitHub',
+ apiRef: githubAuthApiRef,
+};
+
const app = createApp({
apis,
plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => (
+ <SignInPage
+ {...props}
+ auto
+ provider={githubProvider}
+ />
+ ),
+ },
bindRoutes({ bind }) {
```
`SignInPage` component as shown in
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
+2 -26
View File
@@ -48,29 +48,5 @@ The GitLab provider is a structure with three configuration keys:
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `gitlabAuthApi` reference and
`SignInPage` component to `createApp` in `packages/app/src/App.tsx`:
```diff
+ import { gitlabAuthApiRef, SignInConfig, SignInPage } from '@backstage/core';
+ const gitlabProvider: SignInConfig = {
+ id: 'gitlab-auth-provider',
+ title: 'GitLab',
+ message: 'Sign in using GitLab',
+ apiRef: gitlabAuthApiRef,
+};
+
const app = createApp({
apis,
plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => (
+ <SignInPage
+ {...props}
+ auto
+ provider={gitlabProvider}
+ />
+ ),
+ },
bindRoutes({ bind }) {
```
`SignInPage` component as shown in
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
+2 -26
View File
@@ -53,29 +53,5 @@ The Google provider is a structure with two configuration keys:
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `googleAuthApi` reference and
`SignInPage` component to `createApp` in `packages/app/src/App.tsx`:
```diff
+ import { googleAuthApiRef, SignInConfig, SignInPage } from '@backstage/core';
+ const googleProvider: SignInConfig = {
+ id: 'google-auth-provider',
+ title: 'Google',
+ message: 'Sign in using Google',
+ apiRef: googleAuthApiRef,
+};
+
const app = createApp({
apis,
plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => (
+ <SignInPage
+ {...props}
+ auto
+ provider={googleProvider}
+ />
+ ),
+ },
bindRoutes({ bind }) {
```
`SignInPage` component as shown in
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
+96 -75
View File
@@ -1,99 +1,120 @@
---
id: index
title: User Authentication and Authorization in Backstage
description: Documentation on User Authentication and Authorization in Backstage
title: Adding Authentication
description: How to add authentication to a Backstage application
---
## Summary
Authentication in Backstage identifies the user, and provides a way for plugins
to make requests on behalf of a user to third-party services. Backstage can have
zero (guest access), one, or many authentication providers. The default
`@backstage/create-app` template uses guest access for easy startup.
The purpose of the Auth APIs in Backstage is to identify the user, and to
provide a way for plugins to request access to 3rd party services on behalf of
the user (OAuth). This documentation focuses on the implementation of that
solution and how to extend it. For documentation on how to consume the Auth APIs
in a plugin, see [TODO](#TODO).
See [Using authentication and identity](using-auth.md) for tips on using
Backstage identity information in your app or plugins.
### Accessing Third Party Services
## Adding an authentication provider
The main pattern for talking to third party services in Backstage is
user-to-server requests, where short-lived OAuth Access Tokens are requested by
plugins to authenticate calls to external services. These calls can be made
either directly to the services or through a backend plugin or service.
Backstage comes with many common authentication providers in the core library:
By relying on user-to-server calls we keep the coupling between the frontend and
backend low, and provide a much lower barrier for plugins to make use of third
party services. This is in comparison to for example a session-based system,
where access tokens are stored server-side. Such a solution would require a much
deeper coupling between the auth backend plugin, its session storage, and other
backend plugins or separate services. A goal of Backstage is to make it as easy
as possible to create new plugins, and an auth solution based on user-to-server
OAuth helps in that regard.
- [Auth0](auth0/provider.md)
- [Azure](microsoft/provider.md)
- [GitHub](github/provider.md)
- [GitLab](gitlab/provider.md)
- [Google](google/provider.md)
- [Okta](okta/provider.md)
- [OneLogin](onelogin/provider.md)
The method with which frontend plugins request access to third party services is
through [Utility APIs](../api/utility-apis.md) for each service provider. For a
full list of providers, see the
[Utility API References](../reference/utility-apis/README.md).
These built-in providers handle the authentication flow for a particular service
including required scopes, callbacks, etc. These providers are each added to a
Backstage app in a similar way.
### Identity - WIP
### Adding provider configuration
> NOTE: Identity management and the `SignInPage` in Backstage is NOT a method
> for blocking access for unauthorized users, that either requires additional
> backend implementation or a separate service like Google's Identity-Aware
> Proxy. The identity system only serves to provide a personalized experience
> and access to a Backstage Identity Token, which can be passed to backend
> plugins.
Each built-in provider has a configuration block under the `auth` section of
`app-config.yaml`. For example, the GitHub provider:
Identity management is still work in progress, but there are already a couple of
pieces in place that can be used.
```yaml
auth:
environment: development
providers:
github:
development:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
```
#### Identity for Plugin Developers
See the documentation for a particular provider to see what configuration is
needed.
As a plugin developer, there are two main touchpoints for identities: the
`IdentityApi` exported by `@backstage/core` via the `identityApiRef`, and a not
yet existing middleware exported by `@backstage/backend-common`.
The `providers` key may have several authentication providers, if multiple
authentication methods are supported. Each provider may also have configuration
for different authentication environments (development, production, etc). This
allows a single auth backend to serve multiple environments, such as running a
local frontend against a deployed backend. The provider configuration matching
the local `auth.environment` setting will be selected.
The `IdentityApi` gives access to the signed-in user's identity in the frontend.
It provides access to the user's ID, lightweight profile information, and an ID
token used to make authenticated calls within Backstage.
### Adding the provider to the sign-in page
The middleware that will be provided by `@backstage/backend-common` allows
verification of Backstage ID tokens, and optionally loading additional
information about the user. The progress is tracked in
https://github.com/backstage/backstage/issues/1435.
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
`SignInProviderConfig` definitions.
#### Identity for App Developers
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
built-in providers:
If you're setting up your own Backstage app, or want to add a new identity
provider, there are three touchpoints: the frontend auth APIs in
`@backstage/core-api`, the backend auth providers in `auth-backend`, and the
`SignInPage` component configured in the Backstage app via `createApp`.
```diff
# packages/app/src/App.tsx
+ import { githubAuthApiRef, SignInProviderConfig, SignInPage } from '@backstage/core';
The frontend APIs and backend providers are tightly coupled together for each
auth provider, and together they implement an e2e auth flow. Only some auth
providers also act as identity providers though. For example, at the moment of
writing, the Google Auth provider is able to act as a Backstage identity
provider, but the GitHub one can not. For an auth provider to also act as an
identity provider, it needs to implement the `BackstageIdentityApi` in the
frontend, and in the backend it needs to return a `BackstageIdentity` structure.
+ const githubProvider: SignInProviderConfig = {
+ id: 'github-auth-provider',
+ title: 'GitHub',
+ message: 'Sign in using GitHub',
+ apiRef: githubAuthApiRef,
+};
+
const app = createApp({
apis,
plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => (
+ <SignInPage
+ {...props}
+ auto
+ provider={githubProvider}
+ />
+ ),
+ },
bindRoutes({ bind }) {
```
It is up to each provider to implement the mapping between a provider identity
and the corresponding Backstage identity. That is currently still work in
progress, and as a stop-gap for example the Google provider returns the local
part of the user's email as the user ID.
To also allow unauthenticated guest access, use the `providers` prop for
`SignInPage`:
The final piece of the puzzle is the `SignInPage` component that can be
configured as part of the app. Without a sign-in page, Backstage will fall back
to a `guest` identity for all users, without any ID token. To enable sign-in, a
`SignInPage` needs to be configured, which in turn has to supply a user to the
app. The `@backstage/core` package provides a basic sign-in page that allows
both the user and the app developer to choose between a couple of different
sign-in methods, or to designate a single provider that may also be logged in to
automatically.
```diff
const app = createApp({
apis,
plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => (
+ <SignInPage
+ {...props}
+ providers={['guest', githubProvider]}
+ />
+ ),
+ },
bindRoutes({ bind }) {
```
## Further Reading
## Adding a custom authentication provider
More details are provided in dedicated sections of the documentation.
There are generic authentication providers for OAuth2 and SAML. These can reduce
the amount of code needed to implement a custom authentication provider that
adheres to these standards.
- [OAuth](./oauth.md): Description of the generic OAuth flow implemented by the
[auth-backend](https://github.com/backstage/backstage/tree/master/plugins/auth-backend).
- [Glossary](./glossary.md): Glossary of some common terms related to the auth
flows.
Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has
a wide library of authentication strategies for different providers. See
[Add authentication provider](add-auth-provider.md) for details on adding a new
Passport-supported authentication method.
+2 -26
View File
@@ -49,29 +49,5 @@ The Microsoft provider is a structure with three configuration keys:
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `microsoftAuthApi` reference and
`SignInPage` component to `createApp` in `packages/app/src/App.tsx`:
```diff
+ import { microsoftAuthApiRef, SignInConfig, SignInPage } from '@backstage/core';
+ const microsoftProvider: SignInConfig = {
+ id: 'microsoft-auth-provider',
+ title: 'Microsoft Azure',
+ message: 'Sign in using Azure',
+ apiRef: microsoftAuthApiRef,
+};
+
const app = createApp({
apis,
plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => (
+ <SignInPage
+ {...props}
+ auto
+ provider={microsoftProvider}
+ />
+ ),
+ },
bindRoutes({ bind }) {
```
`SignInPage` component as shown in
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
+2 -26
View File
@@ -62,29 +62,5 @@ The values referenced are found on the Application page on your Okta site.
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `oktaAuthApi` reference and
`SignInPage` component to `createApp` in `packages/app/src/App.tsx`:
```diff
+ import { oktaAuthApiRef, SignInConfig, SignInPage } from '@backstage/core';
+ const oktaProvider: SignInConfig = {
+ id: 'okta-auth-provider',
+ title: 'Okta',
+ message: 'Sign in using Okta',
+ apiRef: oktaAuthApiRef,
+};
+
const app = createApp({
apis,
plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => (
+ <SignInPage
+ {...props}
+ auto
+ provider={oktaProvider}
+ />
+ ),
+ },
bindRoutes({ bind }) {
```
`SignInPage` component as shown in
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
+54
View File
@@ -0,0 +1,54 @@
---
id: provider
title: OneLogin Authentication Provider
sidebar_label: OneLogin
description: Adding OneLogin OIDC as an authentication provider in Backstage
---
The Backstage `core-api` package comes with a OneLogin authentication provider
that can authenticate users using OpenID Connect.
## Create an Application on OneLogin
To support OneLogin authentication, you must create an Application:
1. From the OneLogin Admin portal, choose Applications
2. Click `Add App` and select `OpenID Connect`
- Display Name: Backstage (or your custom app name)
3. Click Save
4. Go to the Configuration tab for the Application and set:
- `Login Url`: `http://localhost:3000`
- `Redirect URIs`: `http://localhost:7000/api/auth/onelogin/handler/frame`
5. Click Save
6. Go to the SSO tab for the Application and set:
- `Token Endpoint` > `Authentication Method`: `POST`
7. Click Save
## Configuration
The provider configuration can then be added to your `app-config.yaml` under the
root `auth` configuration:
```yaml
auth:
environment: development
providers:
onelogin:
development:
clientId: ${AUTH_ONELOGIN_CLIENT_ID}
clientSecret: ${AUTH_ONELOGIN_CLIENT_SECRET}
issuer: https://<company>.onelogin.com/oidc/2
```
The OneLogin provider is a structure with three configuration keys; **these are
found on the SSO tab** for the OneLogin Application:
- `clientId`: The client ID
- `clientSecret`: The client secret
- `issuer`: The issuer URL
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `oneloginAuthApi` reference and
`SignInPage` component as shown in
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
+96
View File
@@ -0,0 +1,96 @@
---
id: using-auth
title: Using authentication and identity
description: How to use authentication and identity in Backstage
---
The Auth APIs in Backstage identify the user, and provide a way for plugins to
request access to 3rd party services on behalf of the user (OAuth). This
documentation focuses on the implementation of that solution and how to extend
it.
### Accessing Third Party Services
The main pattern for talking to third party services in Backstage is
user-to-server requests, where short-lived OAuth Access Tokens are requested by
plugins to authenticate calls to external services. These calls can be made
either directly to the services or through a backend plugin or service.
By relying on user-to-server calls we keep the coupling between the frontend and
backend low, and provide a much lower barrier for plugins to make use of third
party services. This is in comparison to for example a session-based system,
where access tokens are stored server-side. Such a solution would require a much
deeper coupling between the auth backend plugin, its session storage, and other
backend plugins or separate services. A goal of Backstage is to make it as easy
as possible to create new plugins, and an auth solution based on user-to-server
OAuth helps in that regard.
The method with which frontend plugins request access to third party services is
through [Utility APIs](../api/utility-apis.md) for each service provider. For a
full list of providers, see the
[Utility API References](../reference/utility-apis/README.md).
### Identity - WIP
> NOTE: Identity management and the `SignInPage` in Backstage is NOT a method
> for blocking access for unauthorized users, that either requires additional
> backend implementation or a separate service like Google's Identity-Aware
> Proxy. The identity system only serves to provide a personalized experience
> and access to a Backstage Identity Token, which can be passed to backend
> plugins.
Identity management is still work in progress, but there are already a couple of
pieces in place that can be used.
#### Identity for Plugin Developers
As a plugin developer, there are two main touchpoints for identities: the
`IdentityApi` exported by `@backstage/core` via the `identityApiRef`, and a not
yet existing middleware exported by `@backstage/backend-common`.
The `IdentityApi` gives access to the signed-in user's identity in the frontend.
It provides access to the user's ID, lightweight profile information, and an ID
token used to make authenticated calls within Backstage.
The middleware that will be provided by `@backstage/backend-common` allows
verification of Backstage ID tokens, and optionally loading additional
information about the user. The progress is tracked in
https://github.com/backstage/backstage/issues/1435.
#### Identity for App Developers
If you're setting up your own Backstage app, or want to add a new identity
provider, there are three touchpoints: the frontend auth APIs in
`@backstage/core-api`, the backend auth providers in `auth-backend`, and the
`SignInPage` component configured in the Backstage app via `createApp`.
The frontend APIs and backend providers are tightly coupled together for each
auth provider, and together they implement an e2e auth flow. Only some auth
providers also act as identity providers though. For example, at the moment of
writing, the Google Auth provider is able to act as a Backstage identity
provider, but the GitHub one can not. For an auth provider to also act as an
identity provider, it needs to implement the `BackstageIdentityApi` in the
frontend, and in the backend it needs to return a `BackstageIdentity` structure.
It is up to each provider to implement the mapping between a provider identity
and the corresponding Backstage identity. That is currently still work in
progress, and as a stop-gap for example the Google provider returns the local
part of the user's email as the user ID.
The final piece of the puzzle is the `SignInPage` component that can be
configured as part of the app. Without a sign-in page, Backstage will fall back
to a `guest` identity for all users, without any ID token. To enable sign-in, a
`SignInPage` needs to be configured, which in turn has to supply a user to the
app. The `@backstage/core` package provides a basic sign-in page that allows
both the user and the app developer to choose between a couple of different
sign-in methods, or to designate a single provider that may also be logged in to
automatically.
## Further Reading
More details are provided in dedicated sections of the documentation.
- [OAuth](./oauth.md): Description of the generic OAuth flow implemented by the
[auth-backend](https://github.com/backstage/backstage/tree/master/plugins/auth-backend).
- [Glossary](./glossary.md): Glossary of some common terms related to the auth
flows.
+22 -2
View File
@@ -54,6 +54,8 @@ versions:check Check Backstage package versioning
prepack Prepares a package for packaging before publishing
postpack Restores the changes made by the prepack command
create-github-app Create new GitHub App in your organization (experimental)
help [command] display help for command
```
@@ -190,10 +192,13 @@ output of `backstage-cli backend:bundle` into an image:
FROM node:14-buster-slim
WORKDIR /app
ADD yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)"
ADD packages/backend/dist/bundle.tar.gz app-config.yaml ./
COPY packages/backend/dist/bundle.tar.gz app-config.yaml ./
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
CMD ["node", "packages/backend"]
```
@@ -606,3 +611,18 @@ the resulting archive in the target `workspace-dir`.
```text
Usage: backstage-cli build-workspace [options] &lt;workspace-dir&gt;
```
## create-github-app
Scope: `root`
Creates a GitHub App in your GitHub organization. This is an alternative to
token-based [GitHub integration](../integrations/github/locations.md). See
[GitHub Apps for Backstage Authentication](../plugins/github-apps.md).
Launches a browser to create the App through GitHub and saves the result as a
YAML file that can be referenced in the GitHub integration configuration.
```text
Usage: backstage-cli create-github-app &lt;github-org&gt;
```
+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'
@@ -519,6 +519,17 @@ consumed by the component, e.g. `artist-api`. This field is optional.
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| [`API`](#kind-api) (default) | Same as this entity, typically `default` | [`consumesApi`, and reverse `apiConsumedBy`](well-known-relations.md#consumesapi-and-apiconsumedby) |
### `spec.dependsOn` [optional]
An array of [entity references](#string-references) to the components and
resources that the component depends on, e.g. `artists-db`. This field is
optional.
| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type |
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
| [`Component`](#kind-component) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
| [`Resource`](#kind-resource) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
## Kind: Template
The following describes the following entity kind:
@@ -1014,6 +1025,17 @@ belongs to, e.g. `artist-engagement-portal`. This field is optional.
| --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- |
| [`System`](#kind-system) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) |
### `spec.dependsOn` [optional]
An array of [entity references](#string-references) to the components and
resources that the resource depends on, e.g. `artist-lookup`. This field is
optional.
| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type |
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
| [`Component`](#kind-component) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
| [`Resource`](#kind-resource) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
## Kind: System
Describes the following entity kind:
@@ -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
@@ -265,3 +259,15 @@ GITHUB_TOKEN=<token> yarn start
If you've also set up the frontend plugin, so you should be ready to go browse
the templates at [localhost:3000/create](http://localhost:3000/create) now!
### Disabling Docker in Docker situation (Optional)
Software Templates use
[Cookiecutter](https://github.com/cookiecutter/cookiecutter) as templating
library. By default it will use the
[spotify/backstage-cookiecutter](<[spotify/backstage-cookiecutter](https://github.com/backstage/backstage/blob/37e35b910afc7d1270855aed0ec4718aba366c91/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile)>)
docker image.
If you are running backstage from a Docker container and you want to avoid
calling a container inside a container, you can set up Cookiecutter in your own
image, this will use the local installation instead.
+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!**
+61
View File
@@ -74,3 +74,64 @@ Note that your list of custom themes overrides the default themes. If you still
want to use the default themes, they are exported as `lightTheme` and
`darkTheme` from
[@backstage/theme](https://www.npmjs.com/package/@backstage/theme).
## Example of a custom theme
```ts
const themeOptions = createThemeOptions({
palette: {
...lightTheme.palette,
primary: {
main: '#123456',
},
secondary: {
main: '#123456',
},
error: {
main: '#123456'
},
warning: {
main: '#123456',
},
info: {
main: '#123456',
},
success: {
main: '#123456',
},
background: {
default: '#123456',
paper: '#123456',
},
banner: {
info: '#123456',
error: '#123456'
text: '#123456'
link: '#123456',
},
errorBackground: '#123456'
warningBackground: '#123456'
infoBackground: '#123456'
navigation: {
background: '#123456',
indicator: '#123456'
color: '#123456'
selectedColor: '#123456',
},
},
defaultPageTheme: 'home',
fontFamily: 'Comic Sans',
/* below drives the header colors */
pageTheme: {
home: genPageTheme(['#123456','#123456'], shapes.wave),
documentation: genPageTheme(['#123456','#123456'], shapes.wave2),
tool: genPageTheme(['#123456','#123456'], shapes.round),
service: genPageTheme(['#123456','#123456'], shapes.wave),
website: genPageTheme(['#123456','#123456'], shapes.wave),
library: genPageTheme(['#123456','#123456'] shapes.wave),
other: genPageTheme(['#123456','#123456'], shapes.wave),
app: genPageTheme(['#123456','#123456'], shapes.wave),
apis: genPageTheme(['#123456','#123456'], shapes.wave),
},
});
```
@@ -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,17 +67,16 @@ 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
In a standard Backstage app created with
[@backstage/create-app](./create-an-app.md), the sidebar is managed inside
`packages/app/src/components/Root.tsx`. The file exports the entire `Sidebar`
element of your app, which you can extend with additional entries by adding new
`SidebarItem` elements.
`packages/app/src/components/Root/Root.tsx`. The file exports the entire
`Sidebar` element of your app, which you can extend with additional entries by
adding new `SidebarItem` elements.
For example, if you install the `api-docs` plugin, a matching `SidebarItem`
could be something like this:
+6
View File
@@ -106,6 +106,12 @@ The install process may also fail if no Python installation is available. Python
is commonly available in most systems already, but if it isn't you can head for
example [here](https://www.python.org/downloads/) to install it.
#### Could not execute command yarn install
Install yarn on your system with `npm install --global yarn` or for more details
refer to the
[prerequisites](https://backstage.io/docs/getting-started/running-backstage-locally#prerequisites)
## Run the app
When the installation is complete you can open the app folder and start the app.
-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!
+2 -2
View File
@@ -2,8 +2,8 @@
id: locations
title: Azure DevOps Locations
sidebar_label: Locations
description:
Integrating source code stored in Azure DevOps into the Backstage catalog
# prettier-ignore
description: Integrating source code stored in Azure DevOps into the Backstage catalog
---
The Azure integration supports loading catalog entities from Azure DevOps.
+2 -2
View File
@@ -2,8 +2,8 @@
id: discovery
title: Bitbucket Discovery
sidebar_label: Discovery
description:
Automatically discovering catalog entities from repositories in Bitbucket
# prettier-ignore
description: Automatically discovering catalog entities from repositories in Bitbucket
---
The Bitbucket integration has a special discovery processor for discovering
+5 -2
View File
@@ -2,8 +2,8 @@
id: locations
title: Bitbucket Locations
sidebar_label: Locations
description:
Integrating source code stored in Bitbucket into the Backstage catalog
# prettier-ignore
description: Integrating source code stored in Bitbucket into the Backstage catalog
---
The Bitbucket integration supports loading catalog entities from bitbucket.org
@@ -38,3 +38,6 @@ a structure with up to four elements:
- `apiBaseUrl` (optional): The URL of the Bitbucket API. For self-hosted
installations, it is commonly at `https://<host>/rest/api/1.0`. For
bitbucket.org, this configuration is not needed as it can be inferred.
> Note: If you are using Bitbucket server you MUST set the username as well as
> the token or appPassword.
+2 -3
View File
@@ -2,9 +2,8 @@
id: discovery
title: GitHub Discovery
sidebar_label: Discovery
description:
Automatically discovering catalog entities from repositories in a GitHub
organization
# prettier-ignore
description: Automatically discovering catalog entities from repositories in a GitHub organization
---
The GitHub integration has a special discovery processor for discovering catalog
@@ -0,0 +1,55 @@
---
id: locations
sidebar_label: Locations
title: Google Cloud Storage Locations
# prettier-ignore
description: Setting up an integration with Google Cloud Storage
---
The Backstage catalog can import entities from a yaml file stored in a GCS
(Google Cloud Storage) bucket. To enable the ingestion of said entities the
`GoogleGcs` integration must be enabled first.
## Configuration
To configure the integration add the appropriate credentials to the Backstage
backend. There are two main ways to do this: by explicitly setting a
`clientEmail` and a `privateKey` or by letting the Google Storage SDK discover
the credentials automatically.
### Explicit credentials
Explicit credentials can be set in the following format:
```yaml
integrations:
googleGcs:
clientEmail: ${GCS_CLIENT_EMAIL}
privateKey: ${GCS_PRIVATE_KEY}
```
Then make sure the environment variables `GCS_CLIENT_EMAIL` and
`GCS_PRIVATE_KEY` are set when you run Backstage.
### Automatic discovery of Google credentials
Since this integration uses the Google Storage SDK, you can also choose to not
provide any explicit credentials and let the SDK discover them automatically.
One of these discovery methods is to provide an environment variable called
`GOOGLE_APPLICATION_CREDENTIALS` and set it to the file path of your JSON
service account key.
For more details and methods to provide credentials to the Google Storage SDK
you can check [this documentation page][google gcs docs].
## Usage
To use this integration to import entities from a GCS bucket go to the Google
console and browse the file you would like to import. Then copy the
`Authenticated URL` and paste it into the text box in the `register component`
form. This url should look like
`https://storage.cloud.google.com/<bucket>/<path>/catalog-info.yaml`.
[google gcs docs]:
https://cloud.google.com/docs/authentication/production#auth-cloud-implicit-nodejs
+2 -3
View File
@@ -2,9 +2,8 @@
id: index
title: Integrations
sidebar_label: Overview
description:
Configuring Backstage to read or publish data with external providers using
integrations
# prettier-ignore
description: Configuring Backstage to read or publish data with external providers using integrations
---
Integrations allow Backstage to read or publish data using external providers
+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
+9 -13
View File
@@ -1,8 +1,8 @@
---
id: switching-sqlite-postgres
title: Switching Backstage from SQLite to PostgreSQL
description:
How to get ready for deploying Backstage to production with PostgreSQL
# prettier-ignore
description: How to get ready for deploying Backstage to production with PostgreSQL
---
The default `@backstage/create-app` database is SQLite, an in-memory database
@@ -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"
+1 -1
View File
@@ -292,7 +292,7 @@ class Index extends React.Component {
Build your own software templates
</ActionBlock.Title>
<ActionBlock.Link
href={`https://github.com/backstage/backstage/blob/master/docs/features/software-templates/extending/index.md`}
href={`https://github.com/backstage/backstage/blob/master/docs/features/software-templates/index.md`}
>
Contribute
</ActionBlock.Link>
+17 -14
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"
@@ -142,6 +132,11 @@
"type": "subcategory",
"label": "LDAP",
"ids": ["integrations/ldap/org"]
},
{
"type": "subcategory",
"label": "Google GCS",
"ids": ["integrations/google-cloud-storage/locations"]
}
],
"Plugins": [
@@ -186,7 +181,6 @@
],
"Auth and identity": [
"auth/index",
"auth/add-auth-provider",
{
"type": "subcategory",
"label": "Included providers",
@@ -196,13 +190,22 @@
"auth/github/provider",
"auth/gitlab/provider",
"auth/google/provider",
"auth/okta/provider"
"auth/okta/provider",
"auth/onelogin/provider"
]
},
"auth/add-auth-provider",
"auth/using-auth",
"auth/auth-backend",
"auth/oauth",
"auth/glossary",
"auth/auth-backend-classes"
"auth/auth-backend-classes",
"auth/glossary"
],
"Deployment": [
"deployment/index",
"deployment/k8s",
"deployment/helm",
"deployment/heroku"
],
"Designing for Backstage": [
"dls/design",
+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"
+3 -1
View File
@@ -38,7 +38,9 @@
"resolutions": {
"**/@roadiehq/**/@backstage/core": "*",
"**/@roadiehq/**/@backstage/plugin-catalog": "*",
"**/@roadiehq/**/@backstage/catalog-model": "*"
"**/@roadiehq/**/@backstage/catalog-model": "*",
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
},
"version": "1.0.0",
"devDependencies": {
+49
View File
@@ -1,5 +1,54 @@
# 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
- Updated dependencies [d0b4ebf22]
- Updated dependencies [1279a3325]
- Updated dependencies [4a4681b1b]
- Updated dependencies [97b60de98]
- Updated dependencies [3f96a9d5a]
- Updated dependencies [b051e770c]
- Updated dependencies [f9c75f7a9]
- Updated dependencies [98dd5da71]
- Updated dependencies [97d53f686]
- Updated dependencies [64d2ce700]
- @backstage/plugin-badges@0.2.0
- @backstage/core@0.7.4
- @backstage/catalog-model@0.7.6
- @backstage/plugin-scaffolder@0.8.2
- @backstage/plugin-catalog-import@0.5.2
- @backstage/plugin-catalog@0.5.3
- @backstage/plugin-org@0.3.12
- @backstage/plugin-github-deployments@0.1.2
- @backstage/cli@0.6.7
## 0.2.21
### Patch Changes
+18 -18
View File
@@ -1,17 +1,17 @@
{
"name": "example-app",
"version": "0.2.21",
"version": "0.2.24",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.7.4",
"@backstage/cli": "^0.6.6",
"@backstage/core": "^0.7.3",
"@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-badges": "^0.1.2",
"@backstage/plugin-catalog": "^0.5.1",
"@backstage/plugin-catalog-import": "^0.5.0",
"@backstage/plugin-api-docs": "^0.4.11",
"@backstage/plugin-badges": "^0.2.0",
"@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",
"@backstage/plugin-cloudbuild": "^0.2.13",
@@ -19,7 +19,7 @@
"@backstage/plugin-explore": "^0.3.2",
"@backstage/plugin-gcp-projects": "^0.2.5",
"@backstage/plugin-github-actions": "^0.4.2",
"@backstage/plugin-github-deployments": "^0.1.1",
"@backstage/plugin-github-deployments": "^0.1.2",
"@backstage/plugin-gitops-profiles": "^0.2.6",
"@backstage/plugin-graphiql": "^0.2.9",
"@backstage/plugin-jenkins": "^0.4.1",
@@ -27,25 +27,25 @@
"@backstage/plugin-kubernetes": "^0.4.2",
"@backstage/plugin-lighthouse": "^0.2.14",
"@backstage/plugin-newrelic": "^0.2.6",
"@backstage/plugin-org": "^0.3.10",
"@backstage/plugin-org": "^0.3.12",
"@backstage/plugin-pagerduty": "0.3.2",
"@backstage/plugin-register-component": "^0.2.12",
"@backstage/plugin-rollbar": "^0.3.3",
"@backstage/plugin-scaffolder": "^0.8.0",
"@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",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.12",
"@roadiehq/backstage-plugin-buildkite": "^0.1.3",
"@roadiehq/backstage-plugin-github-insights": "^0.3.2",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.7.6",
"@roadiehq/backstage-plugin-travis-ci": "^0.4.5",
"@roadiehq/backstage-plugin-buildkite": "^1.0.0",
"@roadiehq/backstage-plugin-github-insights": "^1.0.0",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.0.0",
"@roadiehq/backstage-plugin-travis-ci": "^1.0.0",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
@@ -57,7 +57,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/test-utils": "^0.1.9",
"@backstage/test-utils": "^0.1.10",
"@testing-library/cypress": "^7.0.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+10 -3
View File
@@ -27,7 +27,11 @@ import {
CatalogIndexPage,
catalogPlugin,
} from '@backstage/plugin-catalog';
import { CatalogImportPage } from '@backstage/plugin-catalog-import';
import {
CatalogImportPage,
catalogImportPlugin,
} from '@backstage/plugin-catalog-import';
import {
CostInsightsLabelDataflowInstructionsPage,
CostInsightsPage,
@@ -48,8 +52,8 @@ import React from 'react';
import { hot } from 'react-hot-loader/root';
import { Navigate, Route } from 'react-router';
import { apis } from './apis';
import { EntityPage } from './components/catalog/EntityPage';
import { Root } from './components/Root';
import { entityPage } from './components/catalog/EntityPage';
import { providers } from './identityProviders';
import * as plugins from './plugins';
@@ -82,6 +86,9 @@ const app = createApp({
bind(explorePlugin.externalRoutes, {
catalogEntity: catalogPlugin.routes.catalogEntity,
});
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
});
},
});
@@ -96,7 +103,7 @@ const routes = (
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
<EntityPage />
{entityPage}
</Route>
<Route path="/catalog-import" element={<CatalogImportPage />} />
<Route path="/docs" element={<TechdocsPage />} />
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { CICDSwitcher } from './EntityPage';
import { UrlPatternDiscovery, ApiProvider, ApiRegistry } from '@backstage/core';
import {
buildKiteApiRef,
BuildkiteApi,
} from '@roadiehq/backstage-plugin-buildkite';
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { EntityLayout } from '@backstage/plugin-catalog';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { cicdContent } from './EntityPage';
import { githubActionsApiRef } from '@backstage/plugin-github-actions';
describe('EntityPage Test', () => {
const entity = {
@@ -29,7 +29,7 @@ describe('EntityPage Test', () => {
metadata: {
name: 'ExampleComponent',
annotations: {
'buildkite.com/project-slug': 'exampleProject/examplePipeline',
'github.com/project-slug': 'example/project',
},
},
spec: {
@@ -39,24 +39,36 @@ describe('EntityPage Test', () => {
},
};
const discoveryApi = UrlPatternDiscovery.compile('http://exampleapi.com');
const mockedApi = {
listWorkflowRuns: jest.fn().mockResolvedValue([]),
getWorkflow: jest.fn(),
getWorkflowRun: jest.fn(),
reRunWorkflow: jest.fn(),
downloadJobLogsForWorkflowRun: jest.fn(),
} as jest.Mocked<typeof githubActionsApiRef.T>;
const apis = ApiRegistry.from([
[buildKiteApiRef, new BuildkiteApi({ discoveryApi })],
]);
const apis = ApiRegistry.with(githubActionsApiRef, mockedApi);
describe('CICDSwitcher Test', () => {
it('Should render Buildkite View', async () => {
const renderedComponent = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
<CICDSwitcher entity={entity} />
</ApiProvider>,
),
describe('cicdContent', () => {
it('Should render GitHub Actions View', async () => {
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<EntityLayout>
<EntityLayout.Route path="/ci-cd" title="CI-CD">
{cicdContent}
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
</ApiProvider>,
);
expect(
renderedComponent.getByText(/exampleProject\/examplePipeline/),
).toBeInTheDocument();
expect(rendered.getByText('ExampleComponent')).toBeInTheDocument();
await expect(
rendered.findByText('No Workflow Data'),
).resolves.toBeInTheDocument();
expect(rendered.getByText('Create new Workflow')).toBeInTheDocument();
});
});
});
+390 -455
View File
@@ -14,175 +14,96 @@
* limitations under the License.
*/
import {
ApiEntity,
DomainEntity,
Entity,
GroupEntity,
SystemEntity,
UserEntity,
} from '@backstage/catalog-model';
import React, { ReactNode, useMemo, useState } from 'react';
import BadgeIcon from '@material-ui/icons/CallToAction';
import { EmptyState } from '@backstage/core';
import {
ApiDefinitionCard,
ConsumedApisCard,
ConsumingComponentsCard,
EntityApiDefinitionCard,
EntityConsumingComponentsCard,
EntityHasApisCard,
ProvidedApisCard,
ProvidingComponentsCard,
EntityProvidingComponentsCard,
EntityProvidedApisCard,
EntityConsumedApisCard,
} from '@backstage/plugin-api-docs';
import { EntityBadgesDialog } from '@backstage/plugin-badges';
import {
AboutCard,
EntityAboutCard,
EntityHasComponentsCard,
EntityHasSubcomponentsCard,
EntityHasSystemsCard,
EntityLayout,
EntityLinksCard,
EntityPageLayout,
EntitySystemDiagramCard,
EntitySwitch,
isComponentType,
isKind,
} from '@backstage/plugin-catalog';
import { EntityProvider, useEntity } from '@backstage/plugin-catalog-react';
import {
isPluginApplicableToEntity as isCircleCIAvailable,
Router as CircleCIRouter,
EntityCircleCIContent,
isCircleCIAvailable,
} from '@backstage/plugin-circleci';
import {
isPluginApplicableToEntity as isCloudbuildAvailable,
Router as CloudbuildRouter,
EntityCloudbuildContent,
isCloudbuildAvailable,
} from '@backstage/plugin-cloudbuild';
import {
isPluginApplicableToEntity as isGitHubActionsAvailable,
RecentWorkflowRunsCard,
Router as GitHubActionsRouter,
EntityGithubActionsContent,
EntityRecentGithubActionsRunsCard,
isGithubActionsAvailable,
} from '@backstage/plugin-github-actions';
import {
isPluginApplicableToEntity as isJenkinsAvailable,
LatestRunCard as JenkinsLatestRunCard,
Router as JenkinsRouter,
EntityJenkinsContent,
EntityLatestJenkinsRunCard,
isJenkinsAvailable,
} from '@backstage/plugin-jenkins';
import { Router as KafkaRouter } from '@backstage/plugin-kafka';
import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes';
import { EntityKafkaContent } from '@backstage/plugin-kafka';
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
import {
EmbeddedRouter as LighthouseRouter,
isPluginApplicableToEntity as isLighthouseAvailable,
LastLighthouseAuditCard,
EntityLastLighthouseAuditCard,
EntityLighthouseContent,
isLighthouseAvailable,
} from '@backstage/plugin-lighthouse';
import {
GroupProfileCard,
MembersListCard,
OwnershipCard,
UserProfileCard,
EntityGroupProfileCard,
EntityMembersListCard,
EntityOwnershipCard,
EntityUserProfileCard,
} from '@backstage/plugin-org';
import {
isPluginApplicableToEntity as isPagerDutyAvailable,
PagerDutyCard,
EntityPagerDutyCard,
isPagerDutyAvailable,
} from '@backstage/plugin-pagerduty';
import {
EntityRollbarContent,
isRollbarAvailable,
Router as RollbarRouter,
} from '@backstage/plugin-rollbar';
import { Router as SentryRouter } from '@backstage/plugin-sentry';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import { EntitySentryContent } from '@backstage/plugin-sentry';
import { EntityTechdocsContent } from '@backstage/plugin-techdocs';
import { EntityTodoContent } from '@backstage/plugin-todo';
import { Button, Grid } from '@material-ui/core';
import {
isPluginApplicableToEntity as isBuildkiteAvailable,
Router as BuildkiteRouter,
EntityBuildkiteContent,
isBuildkiteAvailable,
} from '@roadiehq/backstage-plugin-buildkite';
import {
isPluginApplicableToEntity as isGitHubAvailable,
LanguagesCard,
ReadMeCard,
ReleasesCard,
Router as GitHubInsightsRouter,
EntityGithubInsightsContent,
EntityGithubInsightsLanguagesCard,
EntityGithubInsightsReadmeCard,
EntityGithubInsightsReleasesCard,
isGithubInsightsAvailable,
} from '@roadiehq/backstage-plugin-github-insights';
import {
isPluginApplicableToEntity as isPullRequestsAvailable,
PullRequestsStatsCard,
Router as PullRequestsRouter,
EntityGithubPullRequestsContent,
EntityGithubPullRequestsOverviewCard,
isGithubPullRequestsAvailable,
} from '@roadiehq/backstage-plugin-github-pull-requests';
import {
isPluginApplicableToEntity as isTravisCIAvailable,
RecentTravisCIBuildsWidget,
Router as TravisCIRouter,
EntityTravisCIContent,
EntityTravisCIOverviewCard,
isTravisciAvailable,
} from '@roadiehq/backstage-plugin-travis-ci';
import React, { ReactNode, useMemo, useState } from 'react';
import BadgeIcon from '@material-ui/icons/CallToAction';
import { EntityGithubDeploymentsCard } from '@backstage/plugin-github-deployments';
export const CICDSwitcher = ({ entity }: { entity: Entity }) => {
// This component is just an example of how you can implement your company's logic in entity page.
// You can for example enforce that all components of type 'service' should use GitHubActions
switch (true) {
case isJenkinsAvailable(entity):
return <JenkinsRouter entity={entity} />;
case isBuildkiteAvailable(entity):
return <BuildkiteRouter entity={entity} />;
case isCircleCIAvailable(entity):
return <CircleCIRouter entity={entity} />;
case isCloudbuildAvailable(entity):
return <CloudbuildRouter entity={entity} />;
case isTravisCIAvailable(entity):
return <TravisCIRouter entity={entity} />;
case isGitHubActionsAvailable(entity):
return <GitHubActionsRouter entity={entity} />;
default:
return (
<EmptyState
title="No CI/CD available for this entity"
missing="info"
description="You need to add an annotation to your component if you want to enable CI/CD for it. You can read more about annotations in Backstage by clicking the button below."
action={
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/well-known-annotations"
>
Read more
</Button>
}
/>
);
}
};
const RecentCICDRunsSwitcher = ({ entity }: { entity: Entity }) => {
let content: ReactNode;
switch (true) {
case isJenkinsAvailable(entity):
content = <JenkinsLatestRunCard branch="master" variant="gridItem" />;
break;
case isTravisCIAvailable(entity):
content = <RecentTravisCIBuildsWidget entity={entity} />;
break;
case isGitHubActionsAvailable(entity):
content = (
<RecentWorkflowRunsCard entity={entity} limit={4} variant="gridItem" />
);
break;
default:
content = null;
}
if (!content) {
return null;
}
return (
<Grid item sm={6}>
{content}
</Grid>
);
};
export const ErrorsSwitcher = ({ entity }: { entity: Entity }) => {
switch (true) {
case isRollbarAvailable(entity):
return <RollbarRouter entity={entity} />;
default:
return <SentryRouter entity={entity} />;
}
};
const EntityPageLayoutWrapper = (props: { children?: React.ReactNode }) => {
const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
const [badgesDialogOpen, setBadgesDialogOpen] = useState(false);
const extraMenuItems = useMemo(() => {
@@ -197,9 +118,9 @@ const EntityPageLayoutWrapper = (props: { children?: React.ReactNode }) => {
return (
<>
<EntityPageLayout UNSTABLE_extraContextMenuItems={extraMenuItems}>
<EntityLayout UNSTABLE_extraContextMenuItems={extraMenuItems}>
{props.children}
</EntityPageLayout>
</EntityLayout>
<EntityBadgesDialog
open={badgesDialogOpen}
onClose={() => setBadgesDialogOpen(false)}
@@ -208,353 +129,367 @@ const EntityPageLayoutWrapper = (props: { children?: React.ReactNode }) => {
);
};
const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
export const cicdContent = (
<EntitySwitch>
<EntitySwitch.Case if={isJenkinsAvailable}>
<EntityJenkinsContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isBuildkiteAvailable}>
<EntityBuildkiteContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isCircleCIAvailable}>
<EntityCircleCIContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isCloudbuildAvailable}>
<EntityCloudbuildContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isTravisciAvailable}>
<EntityTravisCIContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isGithubActionsAvailable}>
<EntityGithubActionsContent />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EmptyState
title="No CI/CD available for this entity"
missing="info"
description="You need to add an annotation to your component if you want to enable CI/CD for it. You can read more about annotations in Backstage by clicking the button below."
action={
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/well-known-annotations"
>
Read more
</Button>
}
/>
</EntitySwitch.Case>
</EntitySwitch>
);
const cicdCard = (
<EntitySwitch>
<EntitySwitch.Case if={isJenkinsAvailable}>
<Grid item sm={6}>
<EntityLatestJenkinsRunCard branch="master" variant="gridItem" />
</Grid>
</EntitySwitch.Case>
<EntitySwitch.Case if={isTravisciAvailable}>
<Grid item sm={6}>
<EntityTravisCIOverviewCard />
</Grid>
</EntitySwitch.Case>
<EntitySwitch.Case if={isGithubActionsAvailable}>
<Grid item sm={6}>
<EntityRecentGithubActionsRunsCard limit={4} variant="gridItem" />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
);
const errorsContent = (
<EntitySwitch>
<EntitySwitch.Case if={isRollbarAvailable}>
<EntityRollbarContent />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EntitySentryContent />
</EntitySwitch.Case>
</EntitySwitch>
);
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12} md={6}>
<AboutCard entity={entity} variant="gridItem" />
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
{isPagerDutyAvailable(entity) && (
<Grid item xs={12} md={6}>
<EntityProvider entity={entity}>
<PagerDutyCard />
</EntityProvider>
</Grid>
)}
<Grid item xs={12} md={6}>
<EntityLinksCard entity={entity} />
<EntitySwitch>
<EntitySwitch.Case if={isPagerDutyAvailable}>
<Grid item md={6}>
<EntityPagerDutyCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={4} sm={6}>
<EntityLinksCard />
</Grid>
<RecentCICDRunsSwitcher entity={entity} />
{isGitHubAvailable(entity) && (
<>
<Grid item xs={12} md={6}>
<LanguagesCard entity={entity} />
<ReleasesCard entity={entity} />
{cicdCard}
<EntitySwitch>
<EntitySwitch.Case if={e => Boolean(isGithubInsightsAvailable(e))}>
<Grid item md={6}>
<EntityGithubInsightsLanguagesCard />
<EntityGithubInsightsReleasesCard />
</Grid>
<Grid item xs={12} md={6}>
<ReadMeCard entity={entity} maxHeight={350} />
<Grid item md={6}>
<EntityGithubInsightsReadmeCard maxHeight={350} />
</Grid>
</>
)}
{isLighthouseAvailable(entity) && (
<Grid item xs={12} sm={4}>
<LastLighthouseAuditCard variant="gridItem" />
</Grid>
)}
{isPullRequestsAvailable(entity) && (
<Grid item xs={12} sm={4}>
<PullRequestsStatsCard entity={entity} />
</Grid>
)}
<Grid item xs={12} md={6}>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={isLighthouseAvailable}>
<Grid item sm={4}>
<EntityLastLighthouseAuditCard variant="gridItem" />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={e => Boolean(isGithubPullRequestsAvailable(e))}>
<Grid item sm={4}>
<EntityGithubPullRequestsOverviewCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={6}>
<EntityHasSubcomponentsCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityGithubDeploymentsCard />
</Grid>
</Grid>
);
const ComponentApisContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12} md={6}>
<ProvidedApisCard entity={entity} />
</Grid>
<Grid item xs={12} md={6}>
<ConsumedApisCard entity={entity} />
</Grid>
</Grid>
);
const serviceEntityPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/"
title="Overview"
element={<ComponentOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/errors/*"
title="Errors"
element={<ErrorsSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/api/*"
title="API"
element={<ComponentApisContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/kubernetes/*"
title="Kubernetes"
element={<KubernetesRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/pull-requests"
title="Pull Requests"
element={<PullRequestsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/code-insights"
title="Code Insights"
element={<GitHubInsightsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/kafka/*"
title="Kafka"
element={<KafkaRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/todos"
title="TODOs"
element={<EntityTodoContent />}
/>
</EntityPageLayoutWrapper>
);
<EntityLayout.Route path="/ci-cd" title="CI/CD">
{cicdContent}
</EntityLayout.Route>
const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/"
title="Overview"
element={<ComponentOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/lighthouse/*"
title="Lighthouse"
element={<LighthouseRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/errors/*"
title="Errors"
element={<ErrorsSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/kubernetes/*"
title="Kubernetes"
element={<KubernetesRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/pull-requests"
title="Pull Requests"
element={<PullRequestsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/code-insights"
title="Code Insights"
element={<GitHubInsightsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/todos"
title="TODOs"
element={<EntityTodoContent />}
/>
</EntityPageLayoutWrapper>
);
<EntityLayout.Route path="/errors" title="Errors">
{errorsContent}
</EntityLayout.Route>
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<ComponentOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/todos"
title="TODOs"
element={<EntityTodoContent />}
/>
</EntityPageLayoutWrapper>
);
export const ComponentEntityPage = ({ entity }: { entity: Entity }) => {
switch (entity?.spec?.type) {
case 'service':
return <ServiceEntityPage entity={entity} />;
case 'website':
return <WebsiteEntityPage entity={entity} />;
default:
return <DefaultEntityPage entity={entity} />;
}
};
const ApiOverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3}>
<Grid item md={6}>
<AboutCard entity={entity} />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<ProvidingComponentsCard entity={entity} />
<EntityLayout.Route path="/api" title="API">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityProvidedApisCard />
</Grid>
<Grid item md={6}>
<EntityConsumedApisCard />
</Grid>
</Grid>
<Grid item md={6}>
<ConsumingComponentsCard entity={entity} />
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent />
</EntityLayout.Route>
<EntityLayout.Route path="/pull-requests" title="Pull Requests">
<EntityGithubPullRequestsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/code-insights" title="Code Insights">
<EntityGithubInsightsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/kafka" title="Kafka">
<EntityKafkaContent />
</EntityLayout.Route>
<EntityLayout.Route path="/todos" title="TODOs">
<EntityTodoContent />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const websiteEntityPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/ci-cd" title="CI/CD">
{cicdContent}
</EntityLayout.Route>
<EntityLayout.Route path="/lighthouse" title="Lighthouse">
<EntityLighthouseContent />
</EntityLayout.Route>
<EntityLayout.Route path="/errors" title="Errors">
{errorsContent}
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent />
</EntityLayout.Route>
<EntityLayout.Route path="/pull-requests" title="Pull Requests">
<EntityGithubPullRequestsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/code-insights" title="Code Insights">
<EntityGithubInsightsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/todos" title="TODOs">
<EntityTodoContent />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const defaultEntityPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/todos" title="TODOs">
<EntityTodoContent />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const componentPage = (
<EntitySwitch>
<EntitySwitch.Case if={isComponentType('service')}>
{serviceEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case if={isComponentType('website')}>
{websiteEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
const apiPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item md={6}>
<EntityAboutCard />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item md={6}>
<EntityConsumingComponentsCard />
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/definition" title="Definition">
<Grid container spacing={3}>
<Grid item xs={12}>
<EntityApiDefinitionCard />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12}>
<ApiDefinitionCard apiEntity={entity} />
</Grid>
</Grid>
const userPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EntityUserProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const ApiEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<ApiOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/definition/*"
title="Definition"
element={<ApiDefinitionContent entity={entity as ApiEntity} />}
/>
</EntityPageLayoutWrapper>
const groupPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EntityGroupProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
<Grid item xs={12}>
<EntityMembersListCard />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/diagram" title="Diagram">
<EntitySystemDiagramCard />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const UserOverviewContent = ({ entity }: { entity: UserEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<UserProfileCard entity={entity} variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<OwnershipCard entity={entity} variant="gridItem" />
</Grid>
</Grid>
const systemPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const UserEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<UserOverviewContent entity={entity as UserEntity} />}
/>
</EntityPageLayoutWrapper>
const domainPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<GroupProfileCard entity={entity} variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<OwnershipCard entity={entity} variant="gridItem" />
</Grid>
<Grid item xs={12}>
<MembersListCard entity={entity} />
</Grid>
</Grid>
export const entityPage = (
<EntitySwitch>
<EntitySwitch.Case if={isKind('component')} children={componentPage} />
<EntitySwitch.Case if={isKind('api')} children={apiPage} />
<EntitySwitch.Case if={isKind('group')} children={groupPage} />
<EntitySwitch.Case if={isKind('user')} children={userPage} />
<EntitySwitch.Case if={isKind('system')} children={systemPage} />
<EntitySwitch.Case if={isKind('domain')} children={domainPage} />
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
const GroupEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<GroupOverviewContent entity={entity as GroupEntity} />}
/>
</EntityPageLayoutWrapper>
);
const SystemOverviewContent = ({ entity }: { entity: SystemEntity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<AboutCard entity={entity} variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
</Grid>
</Grid>
);
const SystemEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<SystemOverviewContent entity={entity as SystemEntity} />}
/>
<EntityPageLayout.Content
path="/diagram/*"
title="Diagram"
element={<EntitySystemDiagramCard />}
/>
</EntityPageLayoutWrapper>
);
const DomainOverviewContent = ({ entity }: { entity: DomainEntity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<AboutCard entity={entity} variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
</Grid>
</Grid>
);
const DomainEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<DomainOverviewContent entity={entity as DomainEntity} />}
/>
</EntityPageLayoutWrapper>
);
export const EntityPage = () => {
const { entity } = useEntity();
switch (entity?.kind?.toLocaleLowerCase('en-US')) {
case 'component':
return <ComponentEntityPage entity={entity} />;
case 'api':
return <ApiEntityPage entity={entity} />;
case 'group':
return <GroupEntityPage entity={entity} />;
case 'user':
return <UserEntityPage entity={entity} />;
case 'system':
return <SystemEntityPage entity={entity} />;
case 'domain':
return <DomainEntityPage entity={entity} />;
case 'location':
case 'resource':
case 'template':
default:
return <DefaultEntityPage entity={entity} />;
}
};
+4 -4
View File
@@ -27,19 +27,19 @@ export { plugin as GraphiQL } from '@backstage/plugin-graphiql';
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
export { plugin as Rollbar } from '@backstage/plugin-rollbar';
export { plugin as Newrelic } from '@backstage/plugin-newrelic';
export { plugin as TravisCI } from '@roadiehq/backstage-plugin-travis-ci';
export { travisciPlugin } from '@roadiehq/backstage-plugin-travis-ci';
export { plugin as Jenkins } from '@backstage/plugin-jenkins';
export { plugin as ApiDocs } from '@backstage/plugin-api-docs';
export { plugin as GithubPullRequests } from '@roadiehq/backstage-plugin-github-pull-requests';
export { githubPullRequestsPlugin } from '@roadiehq/backstage-plugin-github-pull-requests';
export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects';
export { plugin as Kubernetes } from '@backstage/plugin-kubernetes';
export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild';
export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights';
export { githubInsightsPlugin } from '@roadiehq/backstage-plugin-github-insights';
export { plugin as CatalogImport } from '@backstage/plugin-catalog-import';
export { plugin as UserSettings } from '@backstage/plugin-user-settings';
export { plugin as PagerDuty } from '@backstage/plugin-pagerduty';
export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite';
export { buildkitePlugin } from '@roadiehq/backstage-plugin-buildkite';
export { plugin as Search } from '@backstage/plugin-search';
export { plugin as Org } from '@backstage/plugin-org';
export { plugin as Kafka } from '@backstage/plugin-kafka';
+8
View File
@@ -1,5 +1,13 @@
# @backstage/backend-common
## 0.6.2
### Patch Changes
- b779b5fee: Add UrlReader for Google Cloud Storage
- Updated dependencies [82c66b8cd]
- @backstage/config-loader@0.6.0
## 0.6.1
### Patch Changes
+6 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.6.1",
"version": "0.6.2",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -31,9 +31,10 @@
"dependencies": {
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.4",
"@backstage/config-loader": "^0.5.1",
"@backstage/config-loader": "^0.6.0",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@google-cloud/storage": "^5.8.0",
"@octokit/rest": "^18.0.12",
"@types/cors": "^2.8.6",
"@types/dockerode": "^3.2.1",
@@ -56,6 +57,7 @@
"minimatch": "^3.0.4",
"minimist": "^1.2.5",
"morgan": "^1.10.0",
"raw-body": "^2.4.1",
"selfsigned": "^1.10.7",
"stoppable": "^1.1.0",
"tar": "^6.0.5",
@@ -71,8 +73,8 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.6.5",
"@backstage/test-utils": "^0.1.9",
"@backstage/cli": "^0.6.7",
"@backstage/test-utils": "^0.1.10",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
@@ -0,0 +1,94 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader, JsonObject } from '@backstage/config';
import { getVoidLogger } from '../logging';
import { ReadTreeResponseFactory } from './tree';
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
import { UrlReaderPredicateTuple } from './types';
describe('GcsUrlReader', () => {
const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => {
return GoogleGcsUrlReader.factory({
config: new ConfigReader(config),
logger: getVoidLogger(),
treeResponseFactory: ReadTreeResponseFactory.create({
config: new ConfigReader({}),
}),
});
};
it('does not create a reader without the googleGcs field', () => {
const entries = createReader({
integrations: {},
});
expect(entries).toHaveLength(0);
});
it('creates a reader with credentials correctly configured', () => {
const entries = createReader({
integrations: {
googleGcs: {
privateKey: '--- BEGIN KEY ---- fakekey --- END KEY ---',
clientEmail: 'someone@example.com',
},
},
});
expect(entries).toHaveLength(1);
});
it('creates a reader with default credentials provider', () => {
const entries = createReader({
integrations: {
googleGcs: {},
},
});
expect(entries).toHaveLength(1);
});
describe('predicates', () => {
const readers = createReader({
integrations: {
googleGcs: {},
},
});
const predicate = readers[0].predicate;
it('returns true for the correct google cloud storage host', () => {
expect(predicate(new URL('https://storage.cloud.google.com'))).toBe(true);
});
it('returns true for a url with the full path and the correct host', () => {
expect(
predicate(
new URL(
'https://storage.cloud.google.com/team1/service1/catalog-info.yaml',
),
),
).toBe(true);
});
it('returns false for the wrong hostname under cloud.google.com', () => {
expect(predicate(new URL('https://storage2.cloud.google.com'))).toBe(
false,
);
});
it('returns false for a partially correct host', () => {
expect(predicate(new URL('https://cloud.google.com'))).toBe(false);
});
it('returns false for a completely different host', () => {
expect(predicate(new URL('https://a.example.com/test'))).toBe(false);
});
});
});
@@ -0,0 +1,105 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Storage } from '@google-cloud/storage';
import {
ReaderFactory,
ReadTreeResponse,
SearchResponse,
UrlReader,
} from './types';
import getRawBody from 'raw-body';
import {
GoogleGcsIntegrationConfig,
readGoogleGcsIntegrationConfig,
} from '@backstage/integration';
const GOOGLE_GCS_HOST = 'storage.cloud.google.com';
const parseURL = (
url: string,
): { host: string; bucket: string; key: string } => {
const { host, pathname } = new URL(url);
if (host !== GOOGLE_GCS_HOST) {
throw new Error(`not a valid GCS URL: ${url}`);
}
const [, bucket, ...key] = pathname.split('/');
return {
host: host,
bucket,
key: key.join('/'),
};
};
export class GoogleGcsUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, logger }) => {
if (!config.has('integrations.googleGcs')) {
return [];
}
const gcsConfig = readGoogleGcsIntegrationConfig(
config.getConfig('integrations.googleGcs'),
);
let storage: Storage;
if (!gcsConfig.clientEmail || !gcsConfig.privateKey) {
logger.info(
'googleGcs credentials not found in config. Using default credentials provider.',
);
storage = new Storage();
} else {
storage = new Storage({
credentials: {
client_email: gcsConfig.clientEmail || undefined,
private_key: gcsConfig.privateKey || undefined,
},
});
}
const reader = new GoogleGcsUrlReader(gcsConfig, storage);
const predicate = (url: URL) => url.host === GOOGLE_GCS_HOST;
return [{ reader, predicate }];
};
constructor(
private readonly integration: GoogleGcsIntegrationConfig,
private readonly storage: Storage,
) {}
async read(url: string): Promise<Buffer> {
try {
const { bucket, key } = parseURL(url);
return await getRawBody(
this.storage.bucket(bucket).file(key).createReadStream(),
);
} catch (error) {
throw new Error(`unable to read gcs file from ${url}, ${error}`);
}
}
async readTree(): Promise<ReadTreeResponse> {
throw new Error('GcsUrlReader does not implement readTree');
}
async search(): Promise<SearchResponse> {
throw new Error('GcsUrlReader does not implement search');
}
toString() {
const key = this.integration.privateKey;
return `googleGcs{host=${GOOGLE_GCS_HOST},authed=${Boolean(key)}}`;
}
}
@@ -24,6 +24,7 @@ import { GithubUrlReader } from './GithubUrlReader';
import { GitlabUrlReader } from './GitlabUrlReader';
import { ReadTreeResponseFactory } from './tree';
import { FetchUrlReader } from './FetchUrlReader';
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
type CreateOptions = {
/** Root config object */
@@ -70,6 +71,7 @@ export class UrlReaders {
BitbucketUrlReader.factory,
GithubUrlReader.factory,
GitlabUrlReader.factory,
GoogleGcsUrlReader.factory,
FetchUrlReader.factory,
]),
});
+3 -1
View File
@@ -16,11 +16,13 @@ WORKDIR /app
# Copy repo skeleton first, to avoid unnecessary docker cache invalidation.
# The skeleton contains the package.json of each package in the monorepo,
# and along with yarn.lock and the root package.json, that's enough to run yarn install.
ADD yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)"
# Then copy the rest of the backend bundle, along with any other files we might want.
ADD packages/backend/dist/bundle.tar.gz app-config.yaml ./
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
CMD ["node", "packages/backend", "--config", "app-config.yaml"]
+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(
+20
View File
@@ -1,5 +1,25 @@
# @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
- 97b60de98: Added `stringifyEntityRef`, which always creates a string representation of an entity reference. Also deprecated `serializeEntityRef`, as `stringifyEntityRef` should be used instead.
- 98dd5da71: Add support for multiple links to post-scaffold task summary page
## 0.7.5
### Patch Changes
@@ -14,4 +14,4 @@ spec:
lifecycle: production
owner: team-a
definition:
$text: https://github.com/APIs-guru/openapi-directory/blob/master/APIs/spotify.com/v1/swagger.yaml
$text: https://github.com/APIs-guru/openapi-directory/blob/dab6854d4d599aafb0eb36e6c7ae1fe0c37509b7/APIs/spotify.com/2021.4.2/openapi.yaml
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.7.5",
"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.4",
"@backstage/cli": "^0.6.8",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
@@ -36,6 +36,7 @@ describe('ComponentV1alpha1Validator', () => {
subcomponentOf: 'monolith',
providesApis: ['api-0'],
consumesApis: ['api-0'],
dependsOn: ['resource:resource-0', 'component:component-0'],
system: 'system',
},
};
@@ -160,6 +161,26 @@ describe('ComponentV1alpha1Validator', () => {
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing dependsOn', async () => {
delete (entity as any).spec.dependsOn;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty dependsOn', async () => {
(entity as any).spec.dependsOn = [''];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('rejects undefined dependsOn', async () => {
(entity as any).spec.dependsOn = [undefined];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('accepts no dependsOn', async () => {
(entity as any).spec.dependsOn = [];
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing system', async () => {
delete (entity as any).spec.system;
await expect(validator.check(entity)).resolves.toBe(true);
@@ -34,6 +34,7 @@ export interface ComponentEntityV1alpha1 extends Entity {
subcomponentOf?: string;
providesApis?: string[];
consumesApis?: string[];
dependsOn?: string[];
system?: string;
};
}
@@ -32,6 +32,7 @@ describe('ResourceV1alpha1Validator', () => {
spec: {
type: 'database',
owner: 'me',
dependsOn: ['component:component-0', 'resource:resource-0'],
system: 'system',
},
};
@@ -86,6 +87,26 @@ describe('ResourceV1alpha1Validator', () => {
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('accepts missing dependsOn', async () => {
delete (entity as any).spec.dependsOn;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty dependsOn', async () => {
(entity as any).spec.dependsOn = [''];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('rejects undefined dependsOn', async () => {
(entity as any).spec.dependsOn = [undefined];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('accepts no dependsOn', async () => {
(entity as any).spec.dependsOn = [];
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing system', async () => {
delete (entity as any).spec.system;
await expect(validator.check(entity)).resolves.toBe(true);
@@ -30,6 +30,7 @@ export interface ResourceEntityV1alpha1 extends Entity {
spec: {
type: string;
owner: string;
dependsOn?: string[];
system?: string;
};
}
@@ -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,
@@ -84,6 +84,14 @@
"type": "string",
"minLength": 1
}
},
"dependsOn": {
"type": "array",
"description": "An array of references to other entities that the component depends on to function.",
"items": {
"type": "string",
"minLength": 1
}
}
}
}
@@ -47,6 +47,14 @@
"examples": ["artist-relations-team", "user:john.johnson"],
"minLength": 1
},
"dependsOn": {
"type": "array",
"description": "An array of references to other entities that the resource depends on to function.",
"items": {
"type": "string",
"minLength": 1
}
},
"system": {
"type": "string",
"description": "An entity reference to the system that the resource belongs to.",
@@ -135,6 +135,36 @@
"output": {
"type": "object",
"description": "A templated object describing the outputs of the scaffolding task.",
"properties": {
"links": {
"type": "array",
"description": "A list of external hyperlinks, typically pointing to resources created or updated by the template",
"items": {
"type": "object",
"required": ["url"],
"properties": {
"url": {
"type": "string",
"description": "A url in a standard uri format.",
"examples": ["https://github.com/my-org/my-new-repo"],
"minLength": 1
},
"title": {
"type": "string",
"description": "A user friendly display name for the link.",
"examples": ["View new repo"],
"minLength": 1
},
"icon": {
"type": "string",
"description": "A key representing a visual icon to be displayed in the UI.",
"examples": ["dashboard"],
"minLength": 1
}
}
}
}
},
"additionalProperties": {
"type": "string"
}
+13
View File
@@ -1,5 +1,18 @@
# @backstage/cli
## 0.6.8
### Patch Changes
- 60ce64aa2: Disable hot reloading in CI environments.
## 0.6.7
### Patch Changes
- Updated dependencies [82c66b8cd]
- @backstage/config-loader@0.6.0
## 0.6.6
### Patch Changes
+1 -1
View File
@@ -11,7 +11,7 @@
"incremental": true,
"isolatedModules": true,
"jsx": "react",
"lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2020", "ESNext.Promise"],
"lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2020"],
"module": "ESNext",
"moduleResolution": "node",
"noEmit": false,
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.6.6",
"version": "0.6.8",
"private": false,
"publishConfig": {
"access": "public"
@@ -32,7 +32,7 @@
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.4",
"@backstage/config-loader": "^0.5.1",
"@backstage/config-loader": "^0.6.0",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^4.0.0",
"@lerna/project": "^4.0.0",
@@ -116,11 +116,11 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.6.0",
"@backstage/backend-common": "^0.6.2",
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.3",
"@backstage/core": "^0.7.5",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.9",
"@backstage/test-utils": "^0.1.10",
"@backstage/theme": "^0.2.5",
"@types/diff": "^5.0.0",
"@types/express": "^4.17.6",
+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,
+1 -1
View File
@@ -74,7 +74,7 @@ export const transforms = (options: TransformOptions): Transforms => {
/\.jpe?g$/,
/\.png$/,
/\.frag/,
{ test: /\.svg/, not: [/\.icon\.svg/] },
{ and: [/\.svg/, { not: [/\.icon\.svg/] }] },
/\.xml/,
],
loader: require.resolve('url-loader'),
+16
View File
@@ -1,5 +1,21 @@
# @backstage/config-loader
## 0.6.0
### Minor Changes
- 82c66b8cd: Fix bug where `${...}` was not being escaped to `${...}`
Add support for environment variable substitution in `$include`, `$file` and
`$env` transform values.
- This change allows for including dynamic paths, such as environment specific
secrets by using the same environment variable substitution (`${..}`) already
supported outside of the various include transforms.
- If you are currently using the syntax `${...}` in your include transform values,
you will need to escape the substitution by using `${...}` instead to maintain
the same behavior.
## 0.5.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.5.1",
"version": "0.6.0",
"private": false,
"publishConfig": {
"access": "public",
+9
View File
@@ -1,5 +1,14 @@
# @backstage/core-api
## 0.2.16
### Patch Changes
- 1279a3325: Introduce a `load-chunk` step in the `BootErrorPage` to show make chunk loading
errors visible to the user.
- 4a4681b1b: Improved error messaging for routable extension errors, making it easier to identify the component and mount point that caused the error.
- b051e770c: Fixed a bug with `useRouteRef` where navigating from routes beneath a mount point would often fail.
## 0.2.15
### Patch Changes
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.2.15",
"version": "0.2.16",
"private": false,
"publishConfig": {
"access": "public",
@@ -42,8 +42,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.6.6",
"@backstage/test-utils": "^0.1.9",
"@backstage/cli": "^0.6.7",
"@backstage/test-utils": "^0.1.10",
"@backstage/test-utils-core": "^0.1.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+1 -1
View File
@@ -24,7 +24,7 @@ import { AppConfig } from '@backstage/config';
import { SubRouteRef } from '../routing/types';
export type BootErrorPageProps = {
step: 'load-config';
step: 'load-config' | 'load-chunk';
error: Error;
};

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