diff --git a/.changeset/cost-insights-five-baboons-attack.md b/.changeset/cost-insights-five-baboons-attack.md
new file mode 100644
index 0000000000..f2c070acb4
--- /dev/null
+++ b/.changeset/cost-insights-five-baboons-attack.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-cost-insights': minor
+---
+
+make change ratio optional
diff --git a/.changeset/dry-elephants-doubt.md b/.changeset/dry-elephants-doubt.md
deleted file mode 100644
index 7a9ccc9892..0000000000
--- a/.changeset/dry-elephants-doubt.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-github-deployments': patch
----
-
-Adds extraColumns field to GitHub Deployments card
diff --git a/.changeset/eight-plums-hide.md b/.changeset/eight-plums-hide.md
deleted file mode 100644
index d1f8bcecfe..0000000000
--- a/.changeset/eight-plums-hide.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder-backend': patch
----
-
-Forward user token to scaffolder task for subsequent api requests
diff --git a/.changeset/fair-carrots-tell.md b/.changeset/fair-carrots-tell.md
deleted file mode 100644
index 62dce5c3ce..0000000000
--- a/.changeset/fair-carrots-tell.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli': patch
----
-
-Add `config:docs` command that opens up reference documentation for the local configuration schema in a browser.
diff --git a/.changeset/flat-paws-rule.md b/.changeset/flat-paws-rule.md
deleted file mode 100644
index af7725793e..0000000000
--- a/.changeset/flat-paws-rule.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/core': patch
-'@backstage/plugin-scaffolder': patch
----
-
-Adding Headings for Accessibility on the Scaffolder Plugin
diff --git a/.changeset/fluffy-suns-repair.md b/.changeset/fluffy-suns-repair.md
deleted file mode 100644
index 6f45905219..0000000000
--- a/.changeset/fluffy-suns-repair.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder': patch
----
-
-Respect top-level UI schema keys in scaffolder forms. Allows more advanced RJSF features such as explicit field ordering.
diff --git a/.changeset/fresh-cheetahs-rush.md b/.changeset/fresh-cheetahs-rush.md
deleted file mode 100644
index fe8a9caafb..0000000000
--- a/.changeset/fresh-cheetahs-rush.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli': patch
----
-
-No longer add newly created plugins to `plugins.ts` in the app, as it is no longer needed.
diff --git a/.changeset/good-glasses-build.md b/.changeset/good-glasses-build.md
deleted file mode 100644
index 3d506d3e43..0000000000
--- a/.changeset/good-glasses-build.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/core-api': patch
-'@backstage/core': patch
----
-
-Add support for discovering plugins through the app element tree, removing the need to register them explicitly.
diff --git a/.changeset/healthy-stingrays-shout.md b/.changeset/healthy-stingrays-shout.md
deleted file mode 100644
index c3ce59ce1f..0000000000
--- a/.changeset/healthy-stingrays-shout.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-catalog': patch
----
-
-SystemDiagramCard UI improvements
diff --git a/.changeset/moody-pigs-repeat.md b/.changeset/moody-pigs-repeat.md
deleted file mode 100644
index 396fa80281..0000000000
--- a/.changeset/moody-pigs-repeat.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/core': patch
----
-
-Exported SignInProviderConfig to strongly type SignInPage providers
diff --git a/.changeset/quiet-badgers-cheer.md b/.changeset/quiet-badgers-cheer.md
deleted file mode 100644
index e22bf9492c..0000000000
--- a/.changeset/quiet-badgers-cheer.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Removed `plugins.ts` from the app, as plugins are now discovered through the react tree.
-
-To apply this change to an existing app, simply delete `packages/app/src/plugins.ts` along with the import and usage in `packages/app/src/App.tsx`.
-
-Note that there are a few plugins that require explicit registration, in which case you would need to keep them in `plugins.ts`. The set of plugins that need explicit registration is any plugin that doesn't have a component extension that gets rendered as part of the app element tree. An example of such a plugin in the main Backstage repo is `@backstage/plugin-badges`. In the case of the badges plugin this is because there is not yet a component-based API for adding context menu items to the entity layout.
-
-If you have plugins that still rely on route registration through the `register` method of `createPlugin`, these need to be kept in `plugins.ts` as well. However, it is recommended to migrate these to export an extensions component instead.
diff --git a/.changeset/real-apples-visit.md b/.changeset/real-apples-visit.md
deleted file mode 100644
index b5165f633b..0000000000
--- a/.changeset/real-apples-visit.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-catalog-backend': patch
----
-
-GithubDiscoveryProcessor now excludes archived repositories so they won't be added to Backstage.
diff --git a/.changeset/rude-items-bow.md b/.changeset/rude-items-bow.md
deleted file mode 100644
index 69b76d821e..0000000000
--- a/.changeset/rude-items-bow.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-kubernetes-backend': patch
----
-
-Kubernetes client TLS verification is now configurable and defaults to true
diff --git a/.changeset/six-turtles-sip.md b/.changeset/six-turtles-sip.md
deleted file mode 100644
index e71b04e64d..0000000000
--- a/.changeset/six-turtles-sip.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/backend-common': patch
----
-
-Support configuration of file storage for SQLite databases. Every plugin has its
-own database file at the specified path.
diff --git a/.changeset/sour-plums-enjoy.md b/.changeset/sour-plums-enjoy.md
deleted file mode 100644
index ef68166255..0000000000
--- a/.changeset/sour-plums-enjoy.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Fix system diagram card to be on the system page
-
-To apply the same fix to an existing application, in `EntityPage.tsx` simply move the `` for the `/diagram` path from the `groupPage` down into the `systemPage` element.
diff --git a/.changeset/spotty-pigs-bathe.md b/.changeset/spotty-pigs-bathe.md
new file mode 100644
index 0000000000..0585f335dc
--- /dev/null
+++ b/.changeset/spotty-pigs-bathe.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-tech-radar': patch
+---
+
+Update README for composability
diff --git a/.changeset/stale-carpets-poke.md b/.changeset/stale-carpets-poke.md
deleted file mode 100644
index 4f39e57b71..0000000000
--- a/.changeset/stale-carpets-poke.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@backstage/plugin-catalog-backend': patch
----
-
-Externalize repository processing for BitbucketDiscoveryProcessor.
-
-Add an extension point where you can customize how a matched Bitbucket repository should
-be processed. This can for example be used if you want to generate the catalog-info.yaml
-automatically based on other files in a repository, while taking advantage of the
-build-in repository crawling functionality.
-
-`BitbucketDiscoveryProcessor.fromConfig` now takes an optional parameter `options.parser` where
-you can customize the logic for each repository found. The default parser has the same
-behaviour as before, where it emits an optional location for the matched repository
-and lets the other processors take care of further processing.
-
-```typescript
-const customRepositoryParser: BitbucketRepositoryParser = async function* customRepositoryParser({
- client,
- repository,
-}) {
- // Custom logic for interpret the matching repository.
- // See defaultRepositoryParser for an example
-};
-
-const processor = BitbucketDiscoveryProcessor.fromConfig(env.config, {
- parser: customRepositoryParser,
- logger: env.logger,
-});
-```
diff --git a/.changeset/stale-chefs-retire.md b/.changeset/stale-chefs-retire.md
deleted file mode 100644
index e43c20062a..0000000000
--- a/.changeset/stale-chefs-retire.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/core': patch
----
-
-Adding close button on support menu
diff --git a/.changeset/techdocs-eight-camels-hear.md b/.changeset/techdocs-eight-camels-hear.md
deleted file mode 100644
index 8f2479f6bd..0000000000
--- a/.changeset/techdocs-eight-camels-hear.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@backstage/plugin-techdocs': minor
----
-
-Add feedback link icon in Techdocs Reader that directs to GitLab or GitHub repo issue page with pre-filled title and source link.
-For link to appear, requires `repo_url` and `edit_uri` to be filled in mkdocs.yml, as per https://www.mkdocs.org/user-guide/configuration. An `edit_uri` will need to be specified for self-hosted GitLab/GitHub instances with a different host name.
-To identify issue URL format as GitHub or GitLab, the host name of source in `repo_url` is checked if it contains `gitlab` or `github`. Alternately this is determined by matching to `host` values from `integrations` in app-config.yaml.
diff --git a/.changeset/techdocs-swift-mugs-invent.md b/.changeset/techdocs-swift-mugs-invent.md
deleted file mode 100644
index 912089fb3c..0000000000
--- a/.changeset/techdocs-swift-mugs-invent.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-techdocs': patch
----
-
-Add a test id to the shadow root element of the Reader to access it easily in e2e tests
diff --git a/.changeset/thick-cobras-switch.md b/.changeset/thick-cobras-switch.md
new file mode 100644
index 0000000000..002c724421
--- /dev/null
+++ b/.changeset/thick-cobras-switch.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-proxy-backend': patch
+---
+
+Prefix proxy routes with `/` if not present in configuration
diff --git a/.changeset/tiny-ears-love.md b/.changeset/tiny-ears-love.md
deleted file mode 100644
index 9c3d1613a5..0000000000
--- a/.changeset/tiny-ears-love.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli': patch
----
-
-Added support for Datadog rum events
diff --git a/.changeset/tough-walls-wash.md b/.changeset/tough-walls-wash.md
new file mode 100644
index 0000000000..a45adcfbf6
--- /dev/null
+++ b/.changeset/tough-walls-wash.md
@@ -0,0 +1,9 @@
+---
+'@backstage/catalog-model': patch
+'@backstage/cli': patch
+'@backstage/config-loader': patch
+'@backstage/config': patch
+'@backstage/plugin-scaffolder': patch
+---
+
+Bump `json-schema` dependency from `0.2.5` to `0.3.0`.
diff --git a/.changeset/tricky-yaks-melt.md b/.changeset/tricky-yaks-melt.md
new file mode 100644
index 0000000000..3679e6d1da
--- /dev/null
+++ b/.changeset/tricky-yaks-melt.md
@@ -0,0 +1,6 @@
+---
+'@backstage/techdocs-common': patch
+---
+
+Adding optional config to enable S3-like API for tech-docs using s3ForcePathStyle option.
+This allows providers like LocalStack, Minio and Wasabi (+possibly others) to be used to host tech docs.
diff --git a/.changeset/twenty-peas-deny.md b/.changeset/twenty-peas-deny.md
new file mode 100644
index 0000000000..d149a9337c
--- /dev/null
+++ b/.changeset/twenty-peas-deny.md
@@ -0,0 +1,5 @@
+---
+'@backstage/catalog-client': patch
+---
+
+Allow `filter` parameter to be specified multiple times
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 4e21603c63..b07519824b 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -8,12 +8,16 @@
/docs/features/techdocs @backstage/techdocs-core
/docs/features/search @backstage/techdocs-core
/docs/assets/search @backstage/techdocs-core
+/plugins/code-coverage @alde @nissayeva
+/plugins/code-coverage-backend @alde @nissayeva
/plugins/cost-insights @backstage/silver-lining
/plugins/cloudbuild @trivago/ebarrios
/plugins/search @backstage/techdocs-core
/plugins/search-* @backstage/techdocs-core
/plugins/techdocs @backstage/techdocs-core
/plugins/techdocs-backend @backstage/techdocs-core
+/packages/search-common @backstage/techdocs-core
/packages/techdocs-common @backstage/techdocs-core
/.changeset/cost-insights-* @backstage/silver-lining
+/.changeset/search-* @backstage/techdocs-core
/.changeset/techdocs-* @backstage/techdocs-core
diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt
index 7d0f36d858..694226b8cc 100644
--- a/.github/styles/vocab.txt
+++ b/.github/styles/vocab.txt
@@ -10,6 +10,7 @@ Chai
Changesets
Chanwit
Cloudformation
+Cobertura
Codecov
Codehilite
Config
@@ -17,10 +18,12 @@ Datadog
Debounce
Discoverability
Dockerfile
+dockerfiles
Dockerize
Docusaurus
Env
Expedia
+Fargate
Figma
Firekube
Fiverr
@@ -32,13 +35,16 @@ Hackathons
Heroku
Hostname
Iain
+JaCoCo
JavaScript
Kaewkasi
Knex
Leasot
Lerna
+LocalStack
Luxon
Minikube
+Minio
Mkdocs
Monorepo
Namespaces
@@ -76,6 +82,7 @@ Tolerations
WWW
Weaveworks
Webpack
+XML
Zalando
Zhou
abc
diff --git a/.github/workflows/chromatic-storybook-test.yml b/.github/workflows/chromatic-storybook-test.yml
index bb14e1c38b..89df848c76 100644
--- a/.github/workflows/chromatic-storybook-test.yml
+++ b/.github/workflows/chromatic-storybook-test.yml
@@ -48,5 +48,7 @@ jobs:
- uses: chromaui/action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
+ # projetToken intentionally shared to allow collaborators to run Chromatic on forks
+ # https://www.chromatic.com/docs/custom-ci-provider#run-chromatic-on-external-forks-of-open-source-projects
projectToken: 9tzak77m9nj
storybookBuildDir: 'packages/storybook/dist'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2337a70e2c..0a6d0f8589 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -85,6 +85,9 @@ jobs:
- name: type checking and declarations
run: yarn tsc:full
+ - name: check api reports
+ run: yarn build:api-reports:only --ci
+
- name: build changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
run: yarn lerna -- run build --since origin/master --include-dependencies
diff --git a/.prettierignore b/.prettierignore
index c4e675a5d7..e60d54a466 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,6 +4,7 @@ microsite
coverage
*.hbs
templates
+api-report.md
plugins/scaffolder-backend/sample-templates
.vscode
dist-types
diff --git a/app-config.yaml b/app-config.yaml
index ac59874878..3c4f2d7ea5 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -2,9 +2,9 @@ app:
title: Backstage Example App
baseUrl: http://localhost:3000
googleAnalyticsTrackingId: # UA-000000-0
- datadogRum:
- clientToken: '123456789'
- applicationId: qwerty
+ #datadogRum:
+ # clientToken: '123456789'
+ # applicationId: qwerty
# site: # datadoghq.eu default = datadoghq.com
# env: # optional
diff --git a/contrib/chart/backstage/README.md b/contrib/chart/backstage/README.md
index 7d04a2cf1e..40bb2666bc 100644
--- a/contrib/chart/backstage/README.md
+++ b/contrib/chart/backstage/README.md
@@ -129,6 +129,16 @@ For the CA, create a `configMap` named `--postgres-ca`
kubectl create configmap my-company-backstage-postgres-ca --from-file=ca.crt"
```
+or disable CA mount
+
+```yaml
+backend:
+ postgresCertMountEnabled: false
+
+lighthouse:
+ postgresCertMountEnabled: false
+```
+
> Where the release name contains the chart name "backstage" then only the release name will be used.
Now install the helm chart:
diff --git a/contrib/chart/backstage/templates/_helpers.tpl b/contrib/chart/backstage/templates/_helpers.tpl
index 5d49ba6975..123d4dba5b 100644
--- a/contrib/chart/backstage/templates/_helpers.tpl
+++ b/contrib/chart/backstage/templates/_helpers.tpl
@@ -214,7 +214,7 @@ Postgres port for the backend
{{- .Values.postgresql.service.port }}
{{- else if .Values.appConfig.backend.database.connection.port -}}
{{- .Values.appConfig.backend.database.connection.port }}
-{{ else }}
+{{- else -}}
5432
{{- end -}}
{{- end -}}
diff --git a/contrib/chart/backstage/templates/backend-deployment.yaml b/contrib/chart/backstage/templates/backend-deployment.yaml
index 144fc93e62..0fa6b7fbe2 100644
--- a/contrib/chart/backstage/templates/backend-deployment.yaml
+++ b/contrib/chart/backstage/templates/backend-deployment.yaml
@@ -26,6 +26,13 @@ spec:
{{- end}}
containers:
- name: {{ .Chart.Name }}-backend
+ command: ["node"]
+ args:
+ - "packages/backend"
+ - "--config"
+ - "app-config.yaml"
+ - "--config"
+ - {{ printf "/usr/src/app/%s" (include "backstage.appConfigFilename" .) | quote }}
image: {{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
ports:
@@ -49,16 +56,20 @@ spec:
name: {{ include "backend.postgresql.passwordSecret" .}}
key: postgresql-password
volumeMounts:
+ {{- if .Values.backend.postgresCertMountEnabled }}
- name: postgres-ca
mountPath: {{ include "backstage.backend.postgresCaDir" . }}
+ {{- end }}
- name: app-config
mountPath: {{ printf "/usr/src/app/%s" (include "backstage.appConfigFilename" .) }}
subPath: {{ include "backstage.appConfigFilename" . }}
volumes:
+ {{- if .Values.backend.postgresCertMountEnabled }}
- name: postgres-ca
configMap:
name: {{ include "backstage.fullname" . }}-postgres-ca
+ {{- end }}
- name: app-config
configMap:
name: {{ include "backstage.fullname" . }}-app-config
@@ -83,5 +94,5 @@ spec:
app: backstage
component: backend
- type: ClusterIP
+ type: {{ .Values.backend.serviceType }}
{{- end }}
diff --git a/contrib/chart/backstage/templates/frontend-deployment.yaml b/contrib/chart/backstage/templates/frontend-deployment.yaml
index 9c674cc160..04983513a6 100644
--- a/contrib/chart/backstage/templates/frontend-deployment.yaml
+++ b/contrib/chart/backstage/templates/frontend-deployment.yaml
@@ -1,3 +1,4 @@
+{{- if .Values.frontend.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -46,7 +47,6 @@ spec:
{{- if .Values.global.nodeSelector }}
nodeSelector: {{- toYaml .Values.global.nodeSelector | nindent 8 }}
{{- end }}
-{{- if .Values.frontend.enabled }}
---
apiVersion: v1
kind: Service
@@ -62,5 +62,5 @@ spec:
app: backstage
component: frontend
- type: ClusterIP
+ type: {{ .Values.frontend.serviceType }}
{{- end }}
diff --git a/contrib/chart/backstage/templates/lighthouse-config.yaml b/contrib/chart/backstage/templates/lighthouse-config.yaml
index fdf4bd8b06..7b8cad42dc 100644
--- a/contrib/chart/backstage/templates/lighthouse-config.yaml
+++ b/contrib/chart/backstage/templates/lighthouse-config.yaml
@@ -1,3 +1,4 @@
+{{- if .Values.lighthouse.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
@@ -7,4 +8,5 @@ data:
PGUSER: {{ include "lighthouse.postgresql.user" . | quote }}
PGPORT: {{ include "lighthouse.postgresql.port" . | quote }}
PGHOST: {{ include "lighthouse.postgresql.host" . | quote }}
- PGPATH_TO_CA: {{ include "backstage.lighthouse.postgresCaFilename" . | quote }}
\ No newline at end of file
+ PGPATH_TO_CA: {{ include "backstage.lighthouse.postgresCaFilename" . | quote }}
+{{- end }}
\ No newline at end of file
diff --git a/contrib/chart/backstage/templates/lighthouse-deployment.yaml b/contrib/chart/backstage/templates/lighthouse-deployment.yaml
index d6e1ce19dd..849c48d7a6 100644
--- a/contrib/chart/backstage/templates/lighthouse-deployment.yaml
+++ b/contrib/chart/backstage/templates/lighthouse-deployment.yaml
@@ -51,14 +51,18 @@ spec:
name: {{ include "lighthouse.postgresql.passwordSecret" . }}
key: postgresql-password
+ {{- if .Values.lighthouse.postgresCertMountEnabled }}
volumeMounts:
- name: postgres-ca
mountPath: {{ include "backstage.lighthouse.postgresCaDir" . }}
+ {{- end }}
+ {{- if .Values.lighthouse.postgresCertMountEnabled }}
volumes:
- name: postgres-ca
configMap:
name: {{ include "backstage.fullname" . }}-postgres-ca
+ {{- end }}
{{- if .Values.global.nodeSelector }}
nodeSelector: {{- toYaml .Values.global.nodeSelector | nindent 8 }}
@@ -78,5 +82,5 @@ spec:
app: backstage
component: lighthouse-audit-service
- type: ClusterIP
+ type: {{ .Values.lighthouse.serviceType }}
{{- end }}
diff --git a/contrib/chart/backstage/templates/postgresql-password-backend-secret.yaml b/contrib/chart/backstage/templates/postgresql-password-backend-secret.yaml
new file mode 100644
index 0000000000..a71cef7b21
--- /dev/null
+++ b/contrib/chart/backstage/templates/postgresql-password-backend-secret.yaml
@@ -0,0 +1,15 @@
+{{- if not .Values.postgresql.enabled }}
+---
+apiVersion: v1
+kind: Secret
+type: Opaque
+metadata:
+ name: {{ include "backend.postgresql.passwordSecret" . }}
+ labels:
+ release: {{ .Release.Name }}
+ annotations:
+ "helm.sh/hook": "pre-install,pre-upgrade"
+ "helm.sh/hook-delete-policy": "before-hook-creation"
+data:
+ postgresql-password: {{ .Values.appConfig.backend.database.connection.password | b64enc }}
+{{- end }}
diff --git a/contrib/chart/backstage/templates/postgresql-password-secret.yaml b/contrib/chart/backstage/templates/postgresql-password-lighthouse-secret.yaml
similarity index 51%
rename from contrib/chart/backstage/templates/postgresql-password-secret.yaml
rename to contrib/chart/backstage/templates/postgresql-password-lighthouse-secret.yaml
index e53369adb9..e7374e70db 100644
--- a/contrib/chart/backstage/templates/postgresql-password-secret.yaml
+++ b/contrib/chart/backstage/templates/postgresql-password-lighthouse-secret.yaml
@@ -1,18 +1,4 @@
-{{- if not .Values.postgresql.enabled }}
----
-apiVersion: v1
-kind: Secret
-type: Opaque
-metadata:
- name: {{ include "backend.postgresql.passwordSecret" . }}
- labels:
- release: {{ .Release.Name }}
- annotations:
- "helm.sh/hook": "pre-install,pre-upgrade"
- "helm.sh/hook-delete-policy": "before-hook-creation"
-data:
- postgresql-password: {{ .Values.appConfig.backend.database.connection.password | b64enc }}
-{{- end }}
+{{- if .Values.lighthouse.enabled }}
{{- if not .Values.postgresql.enabled }}
---
apiVersion: v1
@@ -28,3 +14,4 @@ metadata:
data:
postgresql-password: {{ .Values.lighthouse.database.connection.password | b64enc }}
{{- end }}
+{{- end }}
diff --git a/contrib/chart/backstage/values.yaml b/contrib/chart/backstage/values.yaml
index bd80bc22b6..9f70dac6dc 100644
--- a/contrib/chart/backstage/values.yaml
+++ b/contrib/chart/backstage/values.yaml
@@ -3,13 +3,14 @@
# Declare variables to be passed into your templates.
frontend:
- enabled: true
+ enabled: false
replicaCount: 1
image:
repository: martinaif/backstage-k8s-demo-frontend
tag: test1
pullPolicy: IfNotPresent
containerPort: 80
+ serviceType: ClusterIP
resources:
requests:
memory: 128Mi
@@ -23,9 +24,11 @@ backend:
replicaCount: 1
image:
repository: martinaif/backstage-k8s-demo-backend
- tag: test1
+ tag: 20210423T1550
pullPolicy: IfNotPresent
containerPort: 7000
+ serviceType: ClusterIP
+ postgresCertMountEnabled: true
resources:
requests:
memory: 512Mi
@@ -40,6 +43,8 @@ lighthouse:
tag: latest
pullPolicy: IfNotPresent
containerPort: 3003
+ serviceType: ClusterIP
+ postgresCertMountEnabled: true
resources:
requests:
memory: 128Mi
diff --git a/contrib/docker/frontend-with-nginx/Dockerfile b/contrib/docker/frontend-with-nginx/Dockerfile
deleted file mode 100644
index a444c9de83..0000000000
--- a/contrib/docker/frontend-with-nginx/Dockerfile
+++ /dev/null
@@ -1,18 +0,0 @@
-FROM nginx:mainline
-
-# The purpose of this image is to serve the frontend app content separately.
-# By default the Backstage backend uses the app-backend plugin to serve the
-# app from the backend itself, but it may be desirable to move the frontend
-# content serving to a separate deployment, in which case this image can be used.
-
-# This dockerfile requires the app to be built on the host first, as it
-# simply copies in the build output into the image.
-
-RUN apt-get update && apt-get -y install jq && rm -rf /var/lib/apt/lists/*
-
-COPY packages/app/dist /usr/share/nginx/html
-COPY docker/default.conf.template /etc/nginx/conf.d/default.conf.template
-COPY docker/run.sh /usr/local/bin/run.sh
-CMD run.sh
-
-ENV PORT 80
diff --git a/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild b/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild
new file mode 100644
index 0000000000..41f931544f
--- /dev/null
+++ b/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild
@@ -0,0 +1,58 @@
+# The purpose of this image is to serve the frontend app content separately.
+# By default the Backstage backend uses the app-backend plugin to serve the
+# app from the backend itself, but it may be desirable to move the frontend
+# content serving to a separate deployment, in which case this image can be
+# used.
+
+# This dockerfile also performs the build first inside docker. This may come
+# with a build time impact, but is sometimes desirable. If you want to run the
+# build on the host instead, use the file simply named Dockerfile in this folder
+# instead.
+
+# USAGE:
+#
+# - Copy this file and the "docker" folder from this directory to your project
+# root
+#
+# - Update your .dockerignore, make sure that the source folders are not
+# excluded, but do exclude node_modules and build artifacts:
+#
+# .git
+# node_modules
+# packages/*/dist
+# packages/*/node_modules
+# plugins/*/dist
+# plugins/*/node_modules
+#
+# - Update the copy of this file to add configuration arguments to the "build"
+# command, for example:
+#
+# RUN yarn workspace app build --config --config ...
+#
+# - In your project root, run:
+#
+# docker build -t backstage-frontend -f Dockerfile.dockerbuild .
+
+
+
+FROM node:14-buster AS build
+
+RUN mkdir /app
+COPY . /app
+WORKDIR /app
+
+RUN yarn install
+RUN yarn workspace app build
+
+
+
+FROM nginx:mainline
+
+RUN apt-get update && apt-get -y install jq && rm -rf /var/lib/apt/lists/*
+
+COPY --from=build /app/packages/app/dist /usr/share/nginx/html
+COPY docker/default.conf.template /etc/nginx/templates/default.conf.template
+
+COPY docker/inject-config.sh /docker-entrypoint.d/40-inject-config.sh
+
+ENV PORT 80
diff --git a/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild b/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild
new file mode 100644
index 0000000000..1e0134017f
--- /dev/null
+++ b/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild
@@ -0,0 +1,41 @@
+# The purpose of this image is to serve the frontend app content separately.
+# By default the Backstage backend uses the app-backend plugin to serve the
+# app from the backend itself, but it may be desirable to move the frontend
+# content serving to a separate deployment, in which case this image can be
+# used.
+
+# This dockerfile requires the app to be built on the host first, as it
+# simply copies in the build output into the image. If you want to also perform
+# the build itself inside docker, use Dockerfile.build in this folder instead.
+
+
+# USAGE:
+#
+# - Copy this file and the "docker" folder from this directory to your project
+# root
+#
+# - Add the following line to your .dockerignore to make sure that the built
+# frontend actually can be transferred into the docker image:
+#
+# !packages/app/dist
+#
+# - In your project root, run:
+#
+# yarn install
+# yarn tsc
+# yarn build --config --config ...
+# docker build -t backstage-frontend -f Dockerfile.hostbuild .
+
+
+
+FROM nginx:mainline
+
+RUN apt-get update && apt-get -y install jq && rm -rf /var/lib/apt/lists/*
+
+COPY packages/app/dist /usr/share/nginx/html
+COPY docker/default.conf.template /etc/nginx/templates/default.conf.template
+
+COPY docker/inject-config.sh /docker-entrypoint.d/40-inject-config.sh
+
+ENV PORT 80
+
diff --git a/contrib/docker/frontend-with-nginx/README.md b/contrib/docker/frontend-with-nginx/README.md
new file mode 100644
index 0000000000..eae70b8bba
--- /dev/null
+++ b/contrib/docker/frontend-with-nginx/README.md
@@ -0,0 +1,16 @@
+# Frontend with NGINX
+
+This folder contains Docker images that let you run the Backstage frontend as
+a separate image, rather than having it served through the `app-backend` plugin
+from the backend.
+
+Note that when running the frontend like this, the app configuration becomes
+embedded into the actual static JavaScript files at build time. This means that
+you will have to supply the list of configuration files as part of the command
+line at build.
+
+## Usage
+
+There are two variants: one that builds inside Docker, and one that builds on
+the host. See the comments at the top of the individual dockerfiles for usage
+instructions.
diff --git a/contrib/docker/frontend-with-nginx/docker/run.sh b/contrib/docker/frontend-with-nginx/docker/inject-config.sh
similarity index 80%
rename from contrib/docker/frontend-with-nginx/docker/run.sh
rename to contrib/docker/frontend-with-nginx/docker/inject-config.sh
index fdb1742a07..a0cea96ff3 100755
--- a/contrib/docker/frontend-with-nginx/docker/run.sh
+++ b/contrib/docker/frontend-with-nginx/docker/inject-config.sh
@@ -2,13 +2,6 @@
set -Eeuo pipefail
-# Run nginx as root
-sed -i 's/user nginx.*$//' /etc/nginx/nginx.conf
-
-# Write selected env vars to nginx config
-envsubst '$PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
-
-# Inject runtime config into the client
function inject_config() {
# Read runtime config from env in the same way as the @backstage/config-loader package
local config
@@ -41,4 +34,3 @@ function inject_config() {
inject_config
-exec nginx -g 'daemon off;'
diff --git a/contrib/docker/multi-stage-frontend/Dockerfile b/contrib/docker/multi-stage-frontend/Dockerfile
deleted file mode 100644
index 3190687c52..0000000000
--- a/contrib/docker/multi-stage-frontend/Dockerfile
+++ /dev/null
@@ -1,23 +0,0 @@
-FROM node:12-buster AS build
-
-RUN mkdir /app
-COPY . /app
-WORKDIR /app
-
-RUN yarn install
-RUN yarn workspace example-app build
-
-# Contruct backstage-frontend image
-FROM nginx:mainline
-
-RUN apt-get update && apt-get -y install jq && rm -rf /var/lib/apt/lists/*
-
-# Copy from build stage
-COPY --from=build /app/packages/app/dist /usr/share/nginx/html
-
-COPY docker/default.conf.template /etc/nginx/conf.d/default.conf.template
-COPY docker/run.sh /usr/local/bin/run.sh
-
-CMD run.sh
-
-ENV PORT 80
diff --git a/contrib/docker/multi-stage-frontend/README.md b/contrib/docker/multi-stage-frontend/README.md
deleted file mode 100644
index b6ae4fdc6c..0000000000
--- a/contrib/docker/multi-stage-frontend/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Standalone Dockerfile for frontend
-
-This directory contains the resources which will help you build backstage without any requirements
-other than docker itself. It uses a multi-stage Dockerfile to build and ship backstage.
-
-## Usage
-
-You can simply run the following command to build backstage.
-
-```
-# Make sure you are in the root directory of backstage then run
-docker build -t backstage-frontend -f ./contrib/docker/multi-stage-frontend/Dockerfile .
-```
-
-After a successful build, You can simply run backstage frontend with the following command.
-
-```
-docker run -it --rm -p 3080:80 backstage-frontend
-```
diff --git a/contrib/docs/tutorials/aws-fargate-deployment.md b/contrib/docs/tutorials/aws-fargate-deployment.md
new file mode 100644
index 0000000000..bc530441c6
--- /dev/null
+++ b/contrib/docs/tutorials/aws-fargate-deployment.md
@@ -0,0 +1,16 @@
+# Deploying Backstage with AWS Fargate and Aurora
+
+There are any number of ways to deploy backstage containers on AWS. One of the
+simplest from a management and ops perspective is to leverage AWS Fargate and
+Aurora PostgreSQL, where both the container orchestration and database clusters
+are AWS managed services.
+
+However, to use them properly there are a large number of supporting resources
+(VPCs, Security Groups, Load Balancers, Certificate, etc) required. One approach
+is to use [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/) to
+simplify and automate the creation of the entire infrastructure stack along with
+automating the build and deploy of the container.
+
+Check out the [Backstage On AWS](https://github.com/rbogle/backstage-on-aws)
+repository, for a ready to use CDK application for deploying your custom
+Backstage app onto AWS ECS Fargate and Aurora.
diff --git a/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/service.yaml b/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/service.yaml
index 77a113ae5c..e74bfe0b8d 100644
--- a/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/service.yaml
+++ b/contrib/kubernetes/basic_kubernetes_example_with_helm/backstage/templates/service.yaml
@@ -16,7 +16,7 @@ spec:
app.kubernetes.io/name: {{ include "backstage.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
-{{- if .Values.app.enabled }}
+{{- if .Values.backend.enabled }}
---
apiVersion: v1
kind: Service
diff --git a/contrib/terraform/techdocs-s3-storage/readme.md b/contrib/terraform/techdocs-s3-storage/readme.md
new file mode 100644
index 0000000000..9615cc8d13
--- /dev/null
+++ b/contrib/terraform/techdocs-s3-storage/readme.md
@@ -0,0 +1 @@
+This terraform file should create a S3 bucket and setup IAM with a user with an inline policy which gives the user access to the bucket. After you have created the bucket, user and policy you should go to the user in the AWS console and create an access key. This access key should be used as the env variables in step 3a [here](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-aws-s3-bucket-with-techdocs).
diff --git a/contrib/terraform/techdocs-s3-storage/terraform.tf b/contrib/terraform/techdocs-s3-storage/terraform.tf
new file mode 100644
index 0000000000..f256663f43
--- /dev/null
+++ b/contrib/terraform/techdocs-s3-storage/terraform.tf
@@ -0,0 +1,81 @@
+#==========================
+# Variables
+#==========================
+
+variable "backstage_bucket" {
+ default = "backstage_bucket_for_my_corp"
+}
+
+variable "backstage_iam" {
+ default = "backstage"
+}
+
+variable "shared_managed_tag" {
+ default = "terraform_for_my_corp"
+}
+
+#==========================
+# Bucket
+#==========================
+
+resource "aws_s3_bucket" "backstage" {
+ bucket = var.backstage_bucket
+ acl = "private"
+ provider = aws
+
+ lifecycle {
+ prevent_destroy = true
+ }
+
+ server_side_encryption_configuration {
+ rule {
+ apply_server_side_encryption_by_default {
+ sse_algorithm = "AES256"
+ }
+ }
+ }
+
+ tags = {
+ Name = var.backstage_bucket
+ "Managed By Terraform" = var.shared_managed_tag
+ }
+}
+
+resource "aws_s3_bucket_public_access_block" "backstage" {
+ bucket = aws_s3_bucket.backstage.id
+
+ block_public_acls = true
+ block_public_policy = true
+ ignore_public_acls = true
+ restrict_public_buckets = true
+}
+
+
+#==========================
+# IAM
+#==========================
+
+resource "aws_iam_user" "backstage" {
+ name = var.backstage_iam
+}
+
+resource "aws_iam_user_policy" "backstage" {
+ name = var.backstage_iam
+ user = aws_iam_user.backstage.name
+ policy = data.aws_iam_policy_document.backstage_policy.json
+}
+
+data "aws_iam_policy_document" "backstage_policy" {
+ statement {
+ actions = [
+ "s3:PutObject",
+ "s3:GetObject",
+ "s3:ListBucket"
+ ]
+ effect = "Allow"
+ resources = [
+ "${aws_s3_bucket.backstage.arn}",
+ "${aws_s3_bucket.backstage.arn}/*",
+ ]
+ }
+}
diff --git a/cypress/src/integration/integrations.ts b/cypress/src/integration/integrations.ts
new file mode 100644
index 0000000000..5ee077502a
--- /dev/null
+++ b/cypress/src/integration/integrations.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///
+import 'os';
+
+describe('Integrations', () => {
+ describe('ReadTree', () => {
+ it('should work for github', () => {
+ cy.loginAsGuest();
+
+ cy.request('POST', '/api/catalog/locations', {
+ target:
+ 'https://github.com/backstage-verification/test-repo/blob/main/**/*',
+ type: 'url',
+ });
+
+ cy.visit('/catalog');
+ cy.contains('All').click();
+ cy.get('table').should('contain', 'github-repo');
+ cy.get('table').should('contain', 'github-repo-nested');
+ });
+
+ // it('should work for azure', () => {
+ // cy.loginAsGuest();
+
+ // cy.request('POST', '/api/catalog/locations', {
+ // target:
+ // 'https://dev.azure.com/backstage-verification/_git/test-repo?path=*',
+ // type: 'url',
+ // });
+ // });
+
+ it('should work for gitlab', () => {
+ cy.loginAsGuest();
+
+ cy.request('POST', '/api/catalog/locations', {
+ target:
+ 'https://gitlab.com/backstage-verification/test-repo/-/tree/master/**/*',
+ type: 'url',
+ });
+
+ cy.visit('/catalog');
+ cy.contains('All').click();
+ cy.get('table').should('contain', 'gitlab-repo');
+ cy.get('table').should('contain', 'gitlab-repo-nested');
+ });
+
+ it('should work for bitbucket', () => {
+ cy.loginAsGuest();
+
+ cy.request('POST', '/api/catalog/locations', {
+ target:
+ 'https://bitbucket.org/backstage-verification/test-repo/src/master/**/*',
+ type: 'url',
+ });
+
+ cy.visit('/catalog');
+ cy.contains('All').click();
+ cy.get('table').should('contain', 'bitbucket-repo');
+ cy.get('table').should('contain', 'bitbucket-repo-nested');
+ });
+ });
+});
diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md
index ef178b2977..383cf0615a 100644
--- a/docs/deployment/docker.md
+++ b/docs/deployment/docker.md
@@ -232,7 +232,7 @@ package, which is done as follows:
Once the `app-backend` is removed from the backend, you can use your favorite
static file serving method for serving the frontend. An example of how to set up
an NGINX image is available in the
-[contrib folder in the main repo](https://github.com/backstage/backstage/blob/master/contrib/docker/frontend-with-nginx/Dockerfile)
+[contrib folder in the main repo](https://github.com/backstage/backstage/blob/master/contrib/docker/frontend-with-nginx)
Note that if you're building a separate docker build of the frontend you
probably need to adjust `.dockerignore` appropriately. Most likely by making
diff --git a/docs/deployment/helm.md b/docs/deployment/helm.md
index fd36aeb81f..cbfcb1bc57 100644
--- a/docs/deployment/helm.md
+++ b/docs/deployment/helm.md
@@ -1,12 +1,10 @@
---
id: helm
-title: Deploying Backstage with Helm
+title: Deploying with Helm
description: How to deploy Backstage with Helm and Kubernetes
sidebar_label: Helm
---
-# Helm charts
-
An example Backstage app can be deployed in Kubernetes using the
[Backstage Helm charts](https://github.com/backstage/backstage/tree/master/contrib/chart/backstage).
diff --git a/docs/deployment/index.md b/docs/deployment/index.md
index db4ceca6f1..86ad03d1ae 100644
--- a/docs/deployment/index.md
+++ b/docs/deployment/index.md
@@ -30,7 +30,9 @@ 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.
+in AWS, is also available. There is also a contrib guide to deploying Backstage
+with
+[AWS Fargate and Aurora PostgreSQL](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-fargate-deployment.md)
Please consider contributing other deployment guides if you get Backstage set up
on common infrastructure, it would be a great benefit to the community.
diff --git a/docs/deployment/k8s.md b/docs/deployment/k8s.md
index 9dbf447740..2c52e0e308 100644
--- a/docs/deployment/k8s.md
+++ b/docs/deployment/k8s.md
@@ -357,6 +357,16 @@ spec:
name: postgres-secrets
- secretRef:
name: backstage-secrets
+# Uncomment if health checks are enabled in your app:
+# https://backstage.io/docs/plugins/observability#health-checks
+# readinessProbe:
+# httpGet:
+# port: 7000
+# path: /healthcheck
+# livenessProbe:
+# httpGet:
+# port: 7000
+# path: /healthcheck
```
For production deployments, the `image` reference will usually be a full URL to
diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md
index fed24c58d6..10968b427c 100644
--- a/docs/features/kubernetes/installation.md
+++ b/docs/features/kubernetes/installation.md
@@ -23,16 +23,8 @@ cd packages/app
yarn add @backstage/plugin-kubernetes
```
-Once the package has been installed, you need to import the plugin in your app.
-Add the following to `packages/app/src/plugins.ts`:
-
-`plugins.ts`:
-
-```typescript
-export { plugin as Kubernetes } from '@backstage/plugin-kubernetes';
-```
-
-Now, add the "Kubernetes" tab to the catalog entity page. In
+Once the package has been installed, you need to import the plugin in your app
+by adding the "Kubernetes" tab to the catalog entity page. In
`packages/app/src/components/catalog/EntityPage.tsx`, you'll add a router to get
to the tab, and add the tab itself.
diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md
index f4e1f86dd8..eb087a9c5f 100644
--- a/docs/features/software-catalog/descriptor-format.md
+++ b/docs/features/software-catalog/descriptor-format.md
@@ -44,7 +44,7 @@ metadata:
name: artist-web
description: The place to be, for great artists
labels:
- system: public-websites
+ example.com/custom: custom_label_value
annotations:
example.com/service-discovery: artistweb
circleci.com/project-slug: github/example-org/artist-website
@@ -58,6 +58,7 @@ spec:
type: website
lifecycle: production
owner: artist-relations-team
+ system: public-websites
```
This is the same entity as returned in JSON from the software catalog API:
@@ -76,7 +77,7 @@ This is the same entity as returned in JSON from the software catalog API:
"etag": "ZjU2MWRkZWUtMmMxZS00YTZiLWFmMWMtOTE1NGNiZDdlYzNk",
"generation": 1,
"labels": {
- "system": "public-websites"
+ "example.com/custom": "custom_label_value"
},
"links": [{
"url": "https://admin.example-org.com",
@@ -90,7 +91,8 @@ This is the same entity as returned in JSON from the software catalog API:
"spec": {
"lifecycle": "production",
"owner": "artist-relations-team",
- "type": "website"
+ "type": "website",
+ "system": "public-websites"
}
}
```
@@ -639,22 +641,11 @@ A list of strings that can be associated with the template, e.g.
This list will also be used in the frontend to display to the user so you can
potentially search and group templates by these tags.
-### `spec.type` [optional]
+### `spec.type` [required]
-The type of component as a string, e.g. `website`. This field is optional but
-recommended.
-
-The software catalog accepts any type value, but an organization should take
-great care to establish a proper taxonomy for these. Tools including Backstage
-itself may read this field and behave differently depending on its value. For
-example, a website type component may present tooling in the Backstage interface
-that is specific to just websites.
-
-The current set of well-known and common values for this field is:
-
-- `service` - a backend service, typically exposing an API
-- `website` - a website
-- `library` - a software library, such as an npm module or a Java library
+The type of component created by the template, e.g. `website`. This is used for
+filtering templates, and should ideally match the Component
+[spec.type](#spectype-required) created by the template.
### `spec.parameters` [required]
diff --git a/docs/features/software-catalog/installation.md b/docs/features/software-catalog/installation.md
index ff8dfbda68..df473f886f 100644
--- a/docs/features/software-catalog/installation.md
+++ b/docs/features/software-catalog/installation.md
@@ -24,14 +24,8 @@ yarn add @backstage/plugin-catalog
### Adding the Plugin to your `packages/app`
-Add the following entry to the head of your `packages/app/src/plugins.ts`:
-
-```ts
-export { catalogPlugin } from '@backstage/plugin-catalog';
-```
-
-Next we need to install the two pages that the catalog plugin provides. You can
-choose any name for these routes, but we recommend the following:
+Add the two pages that the catalog plugin provides to your app. You can choose
+any name for these routes, but we recommend the following:
```tsx
// packages/app/src/App.tsx
diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md
index 0053c14b79..67b92bd941 100644
--- a/docs/features/software-catalog/well-known-annotations.md
+++ b/docs/features/software-catalog/well-known-annotations.md
@@ -273,6 +273,20 @@ project within your organization.
Specifying this annotation may enable SonarQube related features in Backstage
for that entity.
+### backstage.io/code-coverage
+
+```yaml
+# Example:
+metadata:
+ annotations:
+ backstage.io/code-coverage: scm-only
+```
+
+The value of this annotation controls the code-coverage backstage plugin. If set
+to `scm-only`, the plugin will only take into account files stored in source
+control (e.g. ignoring generated code). If set to `enabled`, all files covered
+by a coverage report will be taken into account.
+
## Deprecated Annotations
The following annotations are deprecated, and only listed here to aid in
diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md
index 105b7e5f96..96eea3c750 100644
--- a/docs/features/software-templates/installation.md
+++ b/docs/features/software-templates/installation.md
@@ -26,14 +26,8 @@ yarn add @backstage/plugin-scaffolder
### Adding the Plugin to your `packages/app`
-Add the following entry to the head of your `packages/app/src/plugins.ts`:
-
-```ts
-export { scaffolderPlugin } from '@backstage/plugin-scaffolder';
-```
-
-Next we need to install the root page that the Scaffolder plugin provides. You
-can choose any path for the route, but we recommend the following:
+Add the root page that the Scaffolder plugin provides to your app. You can
+choose any path for the route, but we recommend the following:
```tsx
import { ScaffolderPage } from '@backstage/plugin-scaffolder';
@@ -265,7 +259,7 @@ the templates at [localhost:3000/create](http://localhost:3000/create) now!
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)>)
+[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
diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md
index c7876f3874..aa38dd42b1 100644
--- a/docs/features/techdocs/configuration.md
+++ b/docs/features/techdocs/configuration.md
@@ -78,6 +78,11 @@ techdocs:
# https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property
endpoint: ${AWS_ENDPOINT}
+ # (Optional) Whether to use path style URLs when communicating with S3.
+ # Defaults to false.
+ # This allows providers like LocalStack, Minio and Wasabi (and possibly others) to be used to host tech docs.
+ s3ForcePathStyle: false
+
# Required when techdocs.publisher.type is set to 'azureBlobStorage'. Skip otherwise.
azureBlobStorage:
diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md
index a96e881849..223b276d68 100644
--- a/docs/features/techdocs/getting-started.md
+++ b/docs/features/techdocs/getting-started.md
@@ -29,14 +29,8 @@ yarn add @backstage/plugin-techdocs
Once the package has been installed, you need to import the plugin in your app.
-Add the following to `packages/app/src/plugins.ts`:
-
-```typescript
-export { plugin as TechDocs } from '@backstage/plugin-techdocs';
-```
-
-Now we can add a route for the TechDocs page. In `packages/app/src/App.tsx`,
-import TechDocsPage and add the following to `FlatRoutes`:
+In `packages/app/src/App.tsx`, import `TechDocsPage` and add the following to
+`FlatRoutes`:
```tsx
import { TechDocsPage } from '@backstage/plugin-techdocs';
diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md
index 2cb769b0e3..6c64e83a76 100644
--- a/docs/features/techdocs/how-to-guides.md
+++ b/docs/features/techdocs/how-to-guides.md
@@ -82,3 +82,84 @@ Caveat: Currently TechDocs sites built using URL Reader will be cached for 30
minutes which means they will not be re-built if new changes are made within 30
minutes. This cache invalidation will be replaced by commit timestamp based
implementation very soon.
+
+## How to use a custom TechDocs home page?
+
+### 1st way: TechDocsCustomHome with a custom configuration
+
+As an example, in your main App.tsx:
+
+```tsx
+import {
+ TechDocsCustomHome,
+ PanelType,
+ TechDocsReaderPage,
+} from '@backstage/plugin-techdocs';
+import { Entity } from '@backstage/catalog-model';
+
+const tabsConfig = [
+ {
+ label: 'Custom Tab',
+ panels: [
+ {
+ title: 'Custom Documents Cards 1',
+ description:
+ 'Explore your internal technical ecosystem through documentation.',
+ panelType: 'DocsCardGrid' as PanelType,
+ // optional, is applied to a container of the panel (excludes header of panel)
+ panelCSS: { maxHeight: '400px', overflow:'auto' },
+ filterPredicate: (entity: Entity) => !!entity.metadata.annotations?.['customCardAnnotationOne'];
+ },
+ {
+ title: 'Custom Documents Cards 2',
+ description:
+ 'Explore your internal technical ecosystem through documentation.',
+ panelType: 'DocsCardGrid' as PanelType,
+ panelCSS: { maxHeight: '400px', overflow:'auto' },
+ filterPredicate: (entity: Entity) => !!entity.metadata.annotations?.['customCardAnnotationTwo'];
+ },
+ ],
+ },
+ {
+ label: 'Overview',
+ panels: [
+ {
+ title: 'Overview',
+ description:
+ 'Explore your internal technical ecosystem through documentation.',
+ panelType: 'DocsTable' as PanelType,
+ filterPredicate: () => true,
+ },
+ ],
+ },
+];
+
+const routes = (
+
+ }
+ />
+ }
+ />
+
+```
+
+An example of tabsConfig that corresponds to the default documentation home page
+can be found at `plugins/techdocs/src/home/components/TechDocsHome.tsx`.
+
+Currently `panelType` has DocsCardGrid and DocsTable available. We currently
+recommend that DocsCardGrid can be optionally vertically stacked by setting a
+maxHeight using `panelCSS`, and DocsTable to be in a tab by itself.
+
+### 2nd way: Custom home page plugin
+
+A custom home page plugin can be built that uses the components extensions
+DocsCardGrid and DocsTable, exported from @backstage/techdocs. They both take a
+array of documentation entities ( i.e.have a 'backstage.io/techdocs-ref'
+annotation ) as an 'entities' attribute.
+
+For a reference to the React structure of the default home page, please refer to
+`plugins/techdocs/src/home/components/TechDocsCustomHome.tsx`.
diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md
index 23015935da..62ee4867c8 100644
--- a/docs/features/techdocs/using-cloud-storage.md
+++ b/docs/features/techdocs/using-cloud-storage.md
@@ -119,6 +119,7 @@ techdocs:
Create a dedicated AWS S3 bucket for the storage of TechDocs sites.
[Refer to the official documentation](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/create-bucket.html).
+[Terraform example](https://github.com/backstage/backstage/blob/master/contrib/terraform/techdocs-s3-storage/terraform.tf).
TechDocs will publish documentation to this bucket and will fetch files from
here to serve documentation in Backstage. Note that the bucket names are
@@ -155,7 +156,9 @@ variables**
You should follow the
[AWS security best practices guide for authentication](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html).
-TechDocs needs access to read files and metadata of the S3 bucket.
+TechDocs needs access to read files and metadata of the S3 bucket. So if you are
+creating a policy for a user you want to make sure it is granted access to
+ListBucket, GetObject and PutObject.
If the environment variables
diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md
index b2f3a37d90..746b4bd940 100644
--- a/docs/getting-started/configure-app-with-plugins.md
+++ b/docs/getting-started/configure-app-with-plugins.md
@@ -27,14 +27,7 @@ package.json. Backstage Apps are set up as monorepos with
[yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/). Since
CircleCI is a frontend UI plugin, it goes in `app` rather than `backend`.
-2. Add the plugin itself to the App:
-
-```js
-// packages/app/src/plugins.ts
-export { plugin as CircleCi } from '@backstage/plugin-circleci';
-```
-
-3. Register the plugin in the entity pages:
+2. Add the `EntityCircleCIContent` extension to the entity pages in the app:
```diff
// packages/app/src/components/catalog/EntityPage.tsx
diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md
index 50ebe722be..f96298a33b 100644
--- a/docs/integrations/datadog-rum/installation.md
+++ b/docs/integrations/datadog-rum/installation.md
@@ -3,28 +3,32 @@ id: installation
title: Datadog RUM Installation
sidebar_label: Installation
# prettier-ignore
-description: Adding Datadog RUM to Your App
+description: Adding Datadog Real User Monitoring (RUM) to Your App
---
-There is a basic [Datadog](https://docs.datadoghq.com/real_user_monitoring/)
+Datadog Real User Monitoring (RUM) allows you to visualize and analyze the
+real-time performance and user journeys of your application's individual users.
+This is an option to profile and monitor the user experience of your Backstage
+installation.
+
+There is a basic [Datadog RUM](https://docs.datadoghq.com/real_user_monitoring/)
integration built into Backstage. You can enable it by adding the following to
-your app configuration:
+your `app-config.yaml`:
```yaml
app:
datadogRum:
clientToken: '123456789'
applicationId: qwerty
- # site: # datadoghq.eu default = datadoghq.com
- # env: # optional
+ # site: datadoghq.eu
+ # env: 'staging'
```
-Replace the clientToken and applicationId with the ones generated for you
-Datadog.
+The `clientToken` and `applicationId` are generated from the Datadog RUM page
+following
+[these instructions](https://docs.datadoghq.com/real_user_monitoring/browser/).
-optional arguments:
+There are two optional arguments:
-```
-site: datadoghq.eu # default equals datadoghq.com
-env: dev # allow to specify the environment
-```
+- `site`: The Datadog site of your organization; defaults to `datadoghq.com`
+- `env`: The application environment for Datadog events (no default)
diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md
index f2f2bc72c8..316f066614 100644
--- a/docs/overview/architecture-overview.md
+++ b/docs/overview/architecture-overview.md
@@ -67,33 +67,41 @@ is available at
### Installing plugins
-Plugins are typically loaded by the UI in your Backstage applications
-`plugins.ts` file. For example,
-[here](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts)
-is that file in the Backstage sample app.
+Plugins are typically installed as React components in your Backstage
+application. For example,
+[here](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx)
+is a file that imports many full-page plugins in the Backstage sample app.
-Plugins can be enabled, and passed configuration in `apis.ts`. For example,
-[here](https://github.com/backstage/backstage/blob/master/packages/app/src/apis.ts)
-is that file in the Backstage sample app.
-
-This is how the Lighthouse plugin would be enabled in a typical Backstage
-application:
+An example of one of these plugin components is the `CatalogIndexPage`, which is
+a full-page view that allows you to browse entities in the Backstage catalog. It
+is installed in the app by importing it and adding it as an element like this:
```tsx
-import { ApiHolder, ApiRegistry } from '@backstage/core';
-import {
- lighthouseApiRef,
- LighthouseRestApi,
-} from '@backstage/plugin-lighthouse';
+import { CatalogIndexPage } from '@backstage/plugin-catalog';
-const builder = ApiRegistry.builder();
+...
-export const lighthouseApi = new LighthouseRestApi(/* URL of the lighthouse microservice! */);
-builder.add(lighthouseApiRef, lighthouseApi);
-
-export default builder.build() as ApiHolder;
+const routes = (
+
+ ...
+ } />
+ ...
+
+);
```
+Note that we use `"/catalog"` as our path to this plugin page, but we can choose
+any route we want for the page, as long as it doesn't collide with the routes
+that we choose for the other plugins in the app.
+
+These components that are exported from plugins are referred to as "Plugin
+Extension Components", or "Extension Components". They are regular React
+components, but in addition to being able to be rendered by React, they also
+contain various pieces of metadata that is used to wire together the entire app.
+Extension components are created using `create*Extension` methods, which you can
+read more about in the
+[composability documentation](../plugins/composability.md).
+
As of this moment, there is no config based install procedure for plugins. Some
code changes are required.
diff --git a/docs/overview/stability-index.md b/docs/overview/stability-index.md
index 4172e4d1b7..6882f3d6c2 100644
--- a/docs/overview/stability-index.md
+++ b/docs/overview/stability-index.md
@@ -153,6 +153,13 @@ re-exported from @backstage/core, and this package should not be used directly.
Stability: See @backstage/core
+### `cost-insights` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/cost-insights)
+
+A frontend plugin that allows users to visualize, understand and optimize your
+team's cloud costs.
+
+Stability: `1`
+
### `create-app` [GitHub](https://github.com/backstage/backstage/tree/master/packages/create-app/)
The CLI used to scaffold new Backstage projects.
@@ -359,7 +366,7 @@ https://github.com/backstage/backstage/issues/2771.
### `tech-radar` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/tech-radar/)
-Visualize the your company's official guidelines of different areas of software
+Visualize your company's official guidelines of different areas of software
development.
Stability: `0`
diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md
index 50c56af7a6..3e20e37cad 100644
--- a/docs/plugins/proxying.md
+++ b/docs/plugins/proxying.md
@@ -36,7 +36,7 @@ Example:
```yaml
# in app-config.yaml
proxy:
- '/simple-example': http://simple.example.com:8080
+ simple-example: http://simple.example.com:8080
'/larger-example/v1':
target: http://larger.example.com:8080/svc.v1
headers:
@@ -46,10 +46,11 @@ proxy:
```
Each key under the proxy configuration entry is a route to match, below the
-prefix that the proxy plugin is mounted on. It must start with a slash. For
-example, if the backend mounts the proxy plugin as `/proxy`, the above
-configuration will lead to the proxy acting on backend requests to
-`/api/proxy/simple-example/...` and `/api/proxy/larger-example/v1/...`.
+prefix that the proxy plugin is mounted on. If it does not start with a slash,
+one will be prefixed automatically. For example, if the backend mounts the proxy
+plugin as `/proxy`, the above configuration will lead to the proxy acting on
+backend requests to `/api/proxy/simple-example/...` and
+`/api/proxy/larger-example/v1/...`.
The value inside each route is either a simple URL string, or an object on the
format accepted by
diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md
index 2aacd02cf2..f81e3794e5 100644
--- a/docs/plugins/structure-of-a-plugin.md
+++ b/docs/plugins/structure-of-a-plugin.md
@@ -100,8 +100,7 @@ There are three things needed for a Backstage app to start making use of a
plugin.
1. Add plugin as dependency in `app/package.json`
-2. `import` plugin in `app/src/plugins.ts`
-3. Import and use one or more plugin extensions, for example in
+2. Import and use one or more plugin extensions, for example in
`app/src/App.tsx`.
Luckily these three steps happen automatically when you create a plugin with the
diff --git a/microsite/data/plugins/circleci.yaml b/microsite/data/plugins/circleci.yaml
index 0acad832fa..cffbedca37 100644
--- a/microsite/data/plugins/circleci.yaml
+++ b/microsite/data/plugins/circleci.yaml
@@ -5,7 +5,7 @@ authorUrl: https://github.com/spotify
category: CI/CD
description: Automate your development process with CI hosted in the cloud or on a private server.
documentation: https://github.com/backstage/backstage/tree/master/plugins/circleci
-iconUrl: https://www.saaves.com/storage/brochure/logo-circleci-icon1583764538.png
+iconUrl: img/circleci.png
npmPackageName: '@backstage/plugin-circleci'
tags:
- ci
diff --git a/microsite/data/plugins/cost-insights.yaml b/microsite/data/plugins/cost-insights.yaml
index daa4f55a02..ec2d162b9d 100644
--- a/microsite/data/plugins/cost-insights.yaml
+++ b/microsite/data/plugins/cost-insights.yaml
@@ -5,7 +5,7 @@ authorUrl: https://github.com/spotify
category: Discovery
description: Visualize, understand and optimize your team's cloud costs.
documentation: https://github.com/backstage/backstage/tree/master/plugins/cost-insights
-iconUrl: https://www.materialui.co/materialIcons/editor/monetization_on_white_192x192.png
+iconUrl: img/cost-insights.png
npmPackageName: '@backstage/plugin-cost-insights'
tags:
- web
diff --git a/microsite/sidebars.json b/microsite/sidebars.json
index 1ab242ba2d..c7b049a53e 100644
--- a/microsite/sidebars.json
+++ b/microsite/sidebars.json
@@ -109,6 +109,11 @@
"integrations/bitbucket/discovery"
]
},
+ {
+ "type": "subcategory",
+ "label": "Datadog",
+ "ids": ["integrations/datadog-rum/installation"]
+ },
{
"type": "subcategory",
"label": "GitHub",
@@ -130,13 +135,13 @@
},
{
"type": "subcategory",
- "label": "LDAP",
- "ids": ["integrations/ldap/org"]
+ "label": "Google GCS",
+ "ids": ["integrations/google-cloud-storage/locations"]
},
{
"type": "subcategory",
- "label": "Google GCS",
- "ids": ["integrations/google-cloud-storage/locations"]
+ "label": "LDAP",
+ "ids": ["integrations/ldap/org"]
}
],
"Plugins": [
diff --git a/microsite/static/img/circleci.png b/microsite/static/img/circleci.png
new file mode 100644
index 0000000000..85f262e4b3
Binary files /dev/null and b/microsite/static/img/circleci.png differ
diff --git a/microsite/static/img/cost-insights.png b/microsite/static/img/cost-insights.png
new file mode 100644
index 0000000000..7296dd3524
Binary files /dev/null and b/microsite/static/img/cost-insights.png differ
diff --git a/mkdocs.yml b/mkdocs.yml
index 031fb56059..7826b3221d 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -7,114 +7,153 @@ plugins:
nav:
- Overview:
- What is Backstage?: 'overview/what-is-backstage.md'
- - Backstage architecture: 'overview/architecture-overview.md'
- - Roadmap: 'overview/roadmap.md'
+ - Architecture overview: 'overview/architecture-overview.md'
+ - Project Roadmap: 'overview/roadmap.md'
- Vision: 'overview/vision.md'
- - The Spotify story: 'overview/background.md'
+ - The Spotify Story: 'overview/background.md'
- Strategies for adopting: 'overview/adopting.md'
+ - Stability Index: 'overview/stability-index.md'
- Logo assets: 'overview/logos.md'
- - Getting started:
+ - Getting Started:
- Getting Started: 'getting-started/index.md'
+ - Create an App: 'getting-started/create-an-app.md'
- Running Backstage locally: 'getting-started/running-backstage-locally.md'
+ - App configuration:
+ - Configuring App with plugins: 'getting-started/configure-app-with-plugins.md'
+ - Customize the look-and-feel of your App: 'getting-started/app-custom-theme.md'
+ - Keeping Backstage Updated: 'getting-started/keeping-backstage-updated.md'
+ - Key Concepts: 'getting-started/concepts.md'
- Contributors: 'getting-started/contributors.md'
- - Demo deployment: https://backstage-demo.roadie.io
- - Production deployments:
- - Create an App: 'getting-started/create-an-app.md'
- - App configuration:
- - Configuring App with plugins: 'getting-started/configure-app-with-plugins.md'
- - Customize the look-and-feel of your App: 'getting-started/app-custom-theme.md'
- - Deployment scenarios:
- - Docker: 'getting-started/deployment-docker.md'
- - Kubernetes: 'getting-started/deployment-k8s.md'
- - Kubernetes and Helm: 'getting-started/deployment-helm.md'
- - Other: 'getting-started/deployment-other.md'
- - Features:
+ - CLI:
+ - Overview: 'cli/index.md'
+ - Commands: 'cli/commands.md'
+ - Core Features:
- Software Catalog:
- Overview: 'features/software-catalog/index.md'
- - Installation: 'features/software-catalog/installation.md'
- - Configuration: 'features/software-catalog/configuration.md'
- - System model: 'features/software-catalog/system-model.md'
+ - Installing in your Backstage App: 'features/software-catalog/installation.md'
+ - Catalog Configuration: 'features/software-catalog/configuration.md'
+ - System Model: 'features/software-catalog/system-model.md'
- YAML File Format: 'features/software-catalog/descriptor-format.md'
- Entity References: 'features/software-catalog/references.md'
- Well-known Annotations: 'features/software-catalog/well-known-annotations.md'
+ - Well-known Relations: 'features/software-catalog/well-known-relations.md'
- Extending the model: 'features/software-catalog/extending-the-model.md'
- External integrations: 'features/software-catalog/external-integrations.md'
- API: 'features/software-catalog/api.md'
- - Software creation templates:
+ - Kubernetes:
+ - Overview: 'features/kubernetes/index.md'
+ - Installation: 'features/kubernetes/installation.md'
+ - Configuration: 'features/kubernetes/configuration.md'
+ - Troubleshooting: 'features/kubernetes/troubleshooting.md'
+ - Software Templates:
- Overview: 'features/software-templates/index.md'
- - Installation: 'features/software-templates/installation.md'
- - Adding templates: 'features/software-templates/adding-templates.md'
- - Extending the Scaffolder:
- - Overview: 'features/software-templates/extending/index.md'
- - Create your own Templater: 'features/software-templates/extending/create-your-own-templater.md'
- - Create your own Publisher: 'features/software-templates/extending/create-your-own-publisher.md'
- - Create your own Preparer: 'features/software-templates/extending/create-your-own-preparer.md'
+ - Installing in your Backstage App: 'features/software-templates/installation.md'
+ - Adding your own Templates: 'features/software-templates/adding-templates.md'
+ - Writing Templates: 'features/software-templates/writing-templates.md'
+ - Builtin Actions: 'features/software-templates/builtin-actions.md'
+ - Writing Custom Actions: 'features/software-templates/writing-custom-actions.md'
+ - Writing Templates (Legacy): 'features/software-templates/legacy.md'
- Backstage Search:
- Overview: 'features/search/README.md'
- - Architecture: 'features/search/architecture.md'
+ - Search Architecture: 'features/search/architecture.md'
- TechDocs:
- Overview: 'features/techdocs/README.md'
- Getting Started: 'features/techdocs/getting-started.md'
- Concepts: 'features/techdocs/concepts.md'
- TechDocs Architecture: 'features/techdocs/architecture.md'
- Creating and Publishing Documentation: 'features/techdocs/creating-and-publishing.md'
- - Configuration: 'features/techdocs/configuration.md'
+ - TechDocs Configuration Options: 'features/techdocs/configuration.md'
- Using Cloud Storage: 'features/techdocs/using-cloud-storage.md'
+ - Configuring CI/CD to generate and publish TechDocs sites: 'features/techdocs/configuring-ci-cd.md'
- HOW TO guides: 'features/techdocs/how-to-guides.md'
- Troubleshooting: 'features/techdocs/troubleshooting.md'
- FAQ: 'features/techdocs/FAQ.md'
- - Kubernetes:
- - Overview: 'features/kubernetes/index.md'
- Integrations:
+ - Overview: 'integrations/index.md'
+ - Azure DevOps:
+ - Locations: 'integrations/azure/locations.md'
+ - Bitbucket:
+ - Locations: 'integrations/bitbucket/locations.md'
+ - Discovery: 'integrations/bitbucket/discovery.md'
+ - Datadog:
+ - Installation: 'integrations/datadog-rum/installation.md'
- GitHub:
+ - Locations: 'integrations/github/locations.md'
+ - Discovery: 'integrations/github/discovery.md'
- Org Data: 'integrations/github/org.md'
- - LDAP:
- - Org Data: 'integrations/ldap/org.md'
+ - GitLab:
+ - Locations: 'integrations/gitlab/locations.md'
- Google Analytics:
- Installation: 'integrations/google-analytics/installation.md'
+ - Google GCS:
+ - Locations: 'integrations/google-cloud-storage/locations.md'
+ - LDAP:
+ - Org Data: 'integrations/ldap/org.md'
- Plugins:
- - Overview: 'plugins/index.md'
+ - Intro to plugins: 'plugins/index.md'
- Existing plugins: 'plugins/existing-plugins.md'
- - Creating a new plugin: 'plugins/create-a-plugin.md'
- - Developing a plugin: 'plugins/plugin-development.md'
+ - Create a Backstage Plugin: 'plugins/create-a-plugin.md'
+ - Plugin Development: 'plugins/plugin-development.md'
- Structure of a plugin: 'plugins/structure-of-a-plugin.md'
+ - Plugin Development: 'plugins/plugin-development.md'
+ - Integrate into the Service Catalog: 'plugins/integrating-plugin-into-service-catalog.md'
- Composability System Migration: 'plugins/composability.md'
- Backends and APIs:
- Proxying: 'plugins/proxying.md'
- - Backstage backend plugin: 'plugins/backend-plugin.md'
+ - Backend plugin: 'plugins/backend-plugin.md'
- Call existing API: 'plugins/call-existing-api.md'
+ - GitHub Apps for Backend Authentication: 'plugins/github-apps.md'
- Testing:
- - Overview: 'plugins/testing.md'
+ - Testing with Jest: 'plugins/testing.md'
- Publishing:
- - Open source and npm: 'plugins/publishing.md'
- - Private/internal (non-open source): 'plugins/publish-private.md'
+ - Publishing: 'plugins/publishing.md'
+ - Publish private: 'plugins/publish-private.md'
+ - Add to Marketplace: 'plugins/add-to-marketplace.md'
- Observability: 'plugins/observability.md'
- Configuration:
- - Overview: 'conf/index.md'
- - Reading Configuration: 'conf/reading.md'
- - Writing Configuration: 'conf/writing.md'
- - Defining Configuration: 'conf/defining.md'
+ - Static Configuration in Backstage: 'conf/index.md'
+ - Reading Backstage Configuration: 'conf/reading.md'
+ - Writing Backstage Configuration: 'conf/writing.md'
+ - Defining Configuration for your Plugin: 'conf/defining.md'
- Authentication and identity:
- - Overview: 'auth/index.md'
- - Add auth provider: 'auth/add-auth-provider.md'
+ - Adding Authentication: 'auth/index.md'
+ - Included providers:
+ - Auth0: 'auth/auth0/provider.md'
+ - Azure: 'auth/microsoft/provider.md'
+ - GitHub: 'auth/github/provider.md'
+ - GitLab: 'auth/gitlab/provider.md'
+ - Google: 'auth/google/provider.md'
+ - Okta: 'auth/okta/provider.md'
+ - OneLogin: 'auth/onelogin/provider.md'
+ - Adding authentication providers: 'auth/add-auth-provider.md'
+ - Using authentication and identity: 'auth/using-auth.md'
- Auth backend: 'auth/auth-backend.md'
- - Auth backend class structure: 'auth/auth-backend-classes.md'
- - OAuth: 'auth/oauth.md'
+ - OAuth and OpenID Connect: 'auth/oauth.md'
+ - Auth backend classes: 'auth/auth-backend-classes.md'
- Glossary: 'auth/glossary.md'
+ - Deployment:
+ - Deploying Backstage: 'deployment/index.md'
+ - Docker: 'deployment/docker.md'
+ - Kubernetes: 'deployment/k8s.md'
+ - Helm: 'deployment/helm.md'
+ - Heroku: 'deployment/heroku.md'
- Designing for Backstage:
- - Backstage Design Language System (DLS): 'dls/design.md'
- - Storybook -- reusable UI components: 'http://backstage.io/storybook'
+ - Design: 'dls/design.md'
- Contributing to Storybook: 'dls/contributing-to-storybook.md'
- - Figma resources: 'dls/figma.md'
+ - Figma: 'dls/figma.md'
- API references:
- - TypeScript APIs:
- - Utilities: 'api/utility-apis.md'
+ - TypeScript API:
+ - Utility APIs: 'api/utility-apis.md'
+ - reference/utility-apis/README: 'reference/utility-apis/README.md'
- createPlugin: 'reference/createPlugin.md'
- - createPlugin-feature-flags: 'reference/createPlugin-feature-flags.md'
+ - createPlugin -feature flags: 'reference/createPlugin-feature-flags.md'
- Backend APIs:
- Backend: 'api/backend.md'
- Tutorials:
- - Overview: 'tutorials/index.md'
+ - Future developer journey: 'tutorials/journey.md'
+ - Monorepo App Setup With Authentication: 'tutorials/quickstart-app-auth.md'
+ - Adding Custom Plugin to Existing Monorepo App: 'tutorials/quickstart-app-plugin.md'
+ - Switching Backstage from SQLite to PostgreSQL: 'tutorials/switching-sqlite-postgres.md'
- Architecture Decision Records (ADRs):
- Overview: 'architecture-decisions/index.md'
- ADR001 - Architecture Decision Record (ADR) log: 'architecture-decisions/adr001-add-adr-log.md'
@@ -129,7 +168,7 @@ nav:
- ADR010 - Luxon Date Library: 'architecture-decisions/adr010-luxon-date-library.md'
- ADR011 - Plugin Package Structure: 'architecture-decisions/adr011-plugin-package-structure.md'
- Support:
- - 'support/support.md'
- - 'support/project-structure.md'
+ - Support and community: 'support/support.md'
+ - Backstage Project Structure: 'support/project-structure.md'
- Glossary: glossary.md
- FAQ: FAQ.md
diff --git a/package.json b/package.json
index d87b98fbdb..b0f79031de 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,9 @@
"start": "yarn workspace example-app start",
"start-backend": "yarn workspace example-backend start",
"build": "lerna run build",
+ "build:api-reports": "tsc && yarn build:api-reports:only",
+ "build:api-reports:only": "ts-node -T -P scripts/tsconfig.json scripts/api-extractor.ts",
+ "build:api-docs": "yarn build:api-reports --docs",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli clean && lerna run clean",
@@ -39,10 +42,16 @@
"**/@roadiehq/**/@backstage/core": "*",
"**/@roadiehq/**/@backstage/plugin-catalog": "*",
"**/@roadiehq/**/@backstage/catalog-model": "*",
+ "**/@microsoft/api-extractor/typescript": "^4.0.3",
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
},
"version": "1.0.0",
+ "dependencies": {
+ "@microsoft/api-extractor": "7.13.2-pr1916.0",
+ "@microsoft/api-documenter": "^7.12.16",
+ "@microsoft/api-extractor-model": "^7.12.5"
+ },
"devDependencies": {
"@changesets/cli": "^2.14.0",
"@octokit/openapi-types": "^2.2.0",
diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md
index 81a7f66f4c..e9d3e63ad0 100644
--- a/packages/app/CHANGELOG.md
+++ b/packages/app/CHANGELOG.md
@@ -1,5 +1,78 @@
# example-app
+## 0.2.26
+
+### Patch Changes
+
+- Updated dependencies [1ce80ff02]
+- Updated dependencies [b98de52ae]
+- Updated dependencies [4c42ecca2]
+- Updated dependencies [c614ede9a]
+- Updated dependencies [9afcac5af]
+- Updated dependencies [07a7806c3]
+- Updated dependencies [f6efa71ee]
+- Updated dependencies [19a4dd710]
+- Updated dependencies [dcd54c7cd]
+- Updated dependencies [da546ce00]
+- Updated dependencies [e0c9ed759]
+- Updated dependencies [6fbd7beca]
+- Updated dependencies [15cbe6815]
+- Updated dependencies [39bdaa004]
+- Updated dependencies [cb8c848a3]
+- Updated dependencies [21fddf452]
+- Updated dependencies [17915e29b]
+- Updated dependencies [6eaecbd81]
+- Updated dependencies [23769512a]
+- Updated dependencies [1a142ae8a]
+ - @backstage/plugin-api-docs@0.4.12
+ - @backstage/plugin-cost-insights@0.8.5
+ - @backstage/plugin-github-actions@0.4.4
+ - @backstage/plugin-catalog-import@0.5.4
+ - @backstage/plugin-circleci@0.2.13
+ - @backstage/plugin-explore@0.3.4
+ - @backstage/plugin-graphiql@0.2.10
+ - @backstage/plugin-jenkins@0.4.2
+ - @backstage/plugin-lighthouse@0.2.15
+ - @backstage/plugin-pagerduty@0.3.3
+ - @backstage/plugin-rollbar@0.3.4
+ - @backstage/plugin-sentry@0.3.9
+ - @backstage/core@0.7.7
+ - @backstage/plugin-scaffolder@0.9.2
+ - @backstage/plugin-catalog@0.5.6
+ - @backstage/plugin-search@0.3.5
+ - @backstage/plugin-techdocs@0.9.0
+
+## 0.2.25
+
+### Patch Changes
+
+- Updated dependencies [94da20976]
+- Updated dependencies [d8cc7e67a]
+- Updated dependencies [4e5c94249]
+- Updated dependencies [99fbef232]
+- Updated dependencies [cb0206b2b]
+- Updated dependencies [1373f4f12]
+- Updated dependencies [29a7e4be8]
+- Updated dependencies [ab07d77f6]
+- Updated dependencies [96728a2af]
+- Updated dependencies [931b21a12]
+- Updated dependencies [937ed39ce]
+- Updated dependencies [87c4f59de]
+- Updated dependencies [55b2fc0c0]
+- Updated dependencies [9a9e7a42f]
+- Updated dependencies [50ce875a0]
+- Updated dependencies [ac6025f63]
+- Updated dependencies [e292e393f]
+- Updated dependencies [479b29124]
+ - @backstage/core@0.7.6
+ - @backstage/cli@0.6.9
+ - @backstage/plugin-scaffolder@0.9.1
+ - @backstage/plugin-catalog-import@0.5.3
+ - @backstage/plugin-catalog@0.5.5
+ - @backstage/theme@0.2.6
+ - @backstage/plugin-code-coverage@0.1.2
+ - @backstage/plugin-techdocs@0.8.0
+
## 0.2.24
### Patch Changes
diff --git a/packages/app/package.json b/packages/app/package.json
index f052d27109..74cabfcaf3 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,42 +1,43 @@
{
"name": "example-app",
- "version": "0.2.24",
+ "version": "0.2.26",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.7.7",
- "@backstage/cli": "^0.6.8",
- "@backstage/core": "^0.7.5",
+ "@backstage/cli": "^0.6.9",
+ "@backstage/core": "^0.7.7",
"@backstage/integration-react": "^0.1.1",
- "@backstage/plugin-api-docs": "^0.4.11",
+ "@backstage/plugin-api-docs": "^0.4.12",
"@backstage/plugin-badges": "^0.2.0",
- "@backstage/plugin-catalog": "^0.5.4",
- "@backstage/plugin-catalog-import": "^0.5.2",
+ "@backstage/plugin-catalog": "^0.5.6",
+ "@backstage/plugin-catalog-import": "^0.5.4",
"@backstage/plugin-catalog-react": "^0.1.3",
- "@backstage/plugin-circleci": "^0.2.12",
+ "@backstage/plugin-circleci": "^0.2.13",
"@backstage/plugin-cloudbuild": "^0.2.13",
- "@backstage/plugin-cost-insights": "^0.8.4",
- "@backstage/plugin-explore": "^0.3.2",
+ "@backstage/plugin-code-coverage": "^0.1.2",
+ "@backstage/plugin-cost-insights": "^0.8.5",
+ "@backstage/plugin-explore": "^0.3.4",
"@backstage/plugin-gcp-projects": "^0.2.5",
- "@backstage/plugin-github-actions": "^0.4.2",
- "@backstage/plugin-graphiql": "^0.2.9",
- "@backstage/plugin-jenkins": "^0.4.1",
+ "@backstage/plugin-github-actions": "^0.4.4",
+ "@backstage/plugin-graphiql": "^0.2.10",
+ "@backstage/plugin-jenkins": "^0.4.2",
"@backstage/plugin-kafka": "^0.2.6",
"@backstage/plugin-kubernetes": "^0.4.2",
- "@backstage/plugin-lighthouse": "^0.2.14",
+ "@backstage/plugin-lighthouse": "^0.2.15",
"@backstage/plugin-newrelic": "^0.2.6",
"@backstage/plugin-org": "^0.3.12",
- "@backstage/plugin-pagerduty": "0.3.2",
- "@backstage/plugin-rollbar": "^0.3.3",
- "@backstage/plugin-scaffolder": "^0.9.0",
- "@backstage/plugin-search": "^0.3.4",
- "@backstage/plugin-sentry": "^0.3.8",
+ "@backstage/plugin-pagerduty": "0.3.3",
+ "@backstage/plugin-rollbar": "^0.3.4",
+ "@backstage/plugin-scaffolder": "^0.9.2",
+ "@backstage/plugin-search": "^0.3.5",
+ "@backstage/plugin-sentry": "^0.3.9",
"@backstage/plugin-shortcuts": "^0.1.1",
"@backstage/plugin-tech-radar": "^0.3.9",
- "@backstage/plugin-techdocs": "^0.7.2",
+ "@backstage/plugin-techdocs": "^0.9.0",
"@backstage/plugin-todo": "^0.1.0",
"@backstage/plugin-user-settings": "^0.2.8",
- "@backstage/theme": "^0.2.5",
+ "@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.12",
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index d4db2b0e63..601134362d 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -102,6 +102,7 @@ import {
EntityTravisCIOverviewCard,
isTravisciAvailable,
} from '@roadiehq/backstage-plugin-travis-ci';
+import { EntityCodeCoverageContent } from '@backstage/plugin-code-coverage';
const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
const [badgesDialogOpen, setBadgesDialogOpen] = useState(false);
@@ -303,6 +304,10 @@ const serviceEntityPage = (
+
+
+
+
@@ -347,6 +352,10 @@ const websiteEntityPage = (
+
+
+
+
diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md
index c194f7e666..68fab7d861 100644
--- a/packages/backend-common/CHANGELOG.md
+++ b/packages/backend-common/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/backend-common
+## 0.6.3
+
+### Patch Changes
+
+- d367f63b5: remove use of deprecated type HelmetOptions
+- b42531cfe: Support configuration of file storage for SQLite databases. Every plugin has its
+ own database file at the specified path.
+
## 0.6.2
### Patch Changes
diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md
new file mode 100644
index 0000000000..58b9aa36d9
--- /dev/null
+++ b/packages/backend-common/api-report.md
@@ -0,0 +1,341 @@
+## API Report File for "@backstage/backend-common"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { AzureIntegration } from '@backstage/integration';
+import { BitbucketIntegration } from '@backstage/integration';
+import { Config } from '@backstage/config';
+import { ConfigReader } from '@backstage/config';
+import cors from 'cors';
+import Docker from 'dockerode';
+import { ErrorRequestHandler } from 'express';
+import express from 'express';
+import { Format } from 'logform';
+import { GithubCredentialsProvider } from '@backstage/integration';
+import { GitHubIntegration } from '@backstage/integration';
+import { GitLabIntegration } from '@backstage/integration';
+import * as http from 'http';
+import { Knex } from 'knex';
+import { Logger } from 'winston';
+import { MergeResult } from 'isomorphic-git';
+import { PushResult } from 'isomorphic-git';
+import { Readable } from 'stream';
+import { ReadCommitResult } from 'isomorphic-git';
+import { RequestHandler } from 'express';
+import { Router } from 'express';
+import { Server } from 'http';
+import * as winston from 'winston';
+import { Writable } from 'stream';
+
+// @public (undocumented)
+export class AzureUrlReader implements UrlReader {
+ constructor(integration: AzureIntegration, deps: {
+ treeResponseFactory: ReadTreeResponseFactory;
+ });
+ // (undocumented)
+ static factory: ReaderFactory;
+ // (undocumented)
+ read(url: string): Promise;
+ // (undocumented)
+ readTree(url: string, options?: ReadTreeOptions): Promise;
+ // (undocumented)
+ search(url: string, options?: SearchOptions): Promise;
+ // (undocumented)
+ toString(): string;
+}
+
+// @public
+export class BitbucketUrlReader implements UrlReader {
+ constructor(integration: BitbucketIntegration, deps: {
+ treeResponseFactory: ReadTreeResponseFactory;
+ });
+ // (undocumented)
+ static factory: ReaderFactory;
+ // (undocumented)
+ read(url: string): Promise;
+ // (undocumented)
+ readTree(url: string, options?: ReadTreeOptions): Promise;
+ // (undocumented)
+ search(url: string, options?: SearchOptions): Promise;
+ // (undocumented)
+ toString(): string;
+}
+
+// @public (undocumented)
+export const coloredFormat: Format;
+
+// @public @deprecated
+export const createDatabase: typeof createDatabaseClient;
+
+// @public
+export function createDatabaseClient(dbConfig: Config, overrides?: Partial): Knex;
+
+// @public (undocumented)
+export function createRootLogger(options?: winston.LoggerOptions, env?: NodeJS.ProcessEnv): winston.Logger;
+
+// @public
+export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl;
+
+// @public (undocumented)
+export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise;
+
+// @public
+export function ensureDatabaseExists(dbConfig: Config, ...databases: Array): Promise;
+
+// @public
+export function errorHandler(options?: ErrorHandlerOptions): ErrorRequestHandler;
+
+// @public (undocumented)
+export type ErrorHandlerOptions = {
+ showStackTraces?: boolean;
+ logger?: Logger;
+ logClientErrors?: boolean;
+};
+
+// @public (undocumented)
+export function getRootLogger(): winston.Logger;
+
+// @public
+export function getVoidLogger(): winston.Logger;
+
+// @public (undocumented)
+export class Git {
+ // (undocumented)
+ add({ dir, filepath, }: {
+ dir: string;
+ filepath: string;
+ }): Promise;
+ // (undocumented)
+ addRemote({ dir, url, remote, }: {
+ dir: string;
+ remote: string;
+ url: string;
+ }): Promise;
+ // (undocumented)
+ clone({ url, dir, ref, }: {
+ url: string;
+ dir: string;
+ ref?: string;
+ }): Promise;
+ // (undocumented)
+ commit({ dir, message, author, committer, }: {
+ dir: string;
+ message: string;
+ author: {
+ name: string;
+ email: string;
+ };
+ committer: {
+ name: string;
+ email: string;
+ };
+ }): Promise;
+ // (undocumented)
+ currentBranch({ dir, fullName, }: {
+ dir: string;
+ fullName?: boolean;
+ }): Promise;
+ // (undocumented)
+ fetch({ dir, remote, }: {
+ dir: string;
+ remote?: string;
+ }): Promise;
+ // (undocumented)
+ static fromAuth: ({ username, password, logger, }: {
+ username?: string | undefined;
+ password?: string | undefined;
+ logger?: Logger | undefined;
+ }) => Git;
+ // (undocumented)
+ init({ dir }: {
+ dir: string;
+ }): Promise;
+ // (undocumented)
+ merge({ dir, theirs, ours, author, committer, }: {
+ dir: string;
+ theirs: string;
+ ours?: string;
+ author: {
+ name: string;
+ email: string;
+ };
+ committer: {
+ name: string;
+ email: string;
+ };
+ }): Promise;
+ // (undocumented)
+ push({ dir, remote }: {
+ dir: string;
+ remote: string;
+ }): Promise;
+ // (undocumented)
+ readCommit({ dir, sha, }: {
+ dir: string;
+ sha: string;
+ }): Promise;
+ // (undocumented)
+ resolveRef({ dir, ref, }: {
+ dir: string;
+ ref: string;
+ }): Promise;
+}
+
+// @public
+export class GithubUrlReader implements UrlReader {
+ constructor(integration: GitHubIntegration, deps: {
+ treeResponseFactory: ReadTreeResponseFactory;
+ credentialsProvider: GithubCredentialsProvider;
+ });
+ // (undocumented)
+ static factory: ReaderFactory;
+ // (undocumented)
+ read(url: string): Promise;
+ // (undocumented)
+ readTree(url: string, options?: ReadTreeOptions): Promise;
+ // (undocumented)
+ search(url: string, options?: SearchOptions): Promise;
+ // (undocumented)
+ toString(): string;
+}
+
+// @public (undocumented)
+export class GitlabUrlReader implements UrlReader {
+ constructor(integration: GitLabIntegration, deps: {
+ treeResponseFactory: ReadTreeResponseFactory;
+ });
+ // (undocumented)
+ static factory: ReaderFactory;
+ // (undocumented)
+ read(url: string): Promise;
+ // (undocumented)
+ readTree(url: string, options?: ReadTreeOptions): Promise;
+ // (undocumented)
+ search(url: string, options?: SearchOptions): Promise;
+ // (undocumented)
+ toString(): string;
+}
+
+// @public
+export function loadBackendConfig(options: Options): Promise;
+
+// @public
+export function notFoundHandler(): RequestHandler;
+
+// @public
+export interface PluginDatabaseManager {
+ getClient(): Promise;
+}
+
+// @public
+export type PluginEndpointDiscovery = {
+ getBaseUrl(pluginId: string): Promise;
+ getExternalBaseUrl(pluginId: string): Promise;
+};
+
+// @public (undocumented)
+export type ReadTreeResponse = {
+ files(): Promise;
+ archive(): Promise;
+ dir(options?: ReadTreeResponseDirOptions): Promise;
+ etag: string;
+};
+
+// @public
+export type ReadTreeResponseFile = {
+ path: string;
+ content(): Promise;
+};
+
+// @public
+export function requestLoggingHandler(logger?: Logger): RequestHandler;
+
+// @public
+export function resolvePackagePath(name: string, ...paths: string[]): string;
+
+// @public (undocumented)
+export const runDockerContainer: ({ imageName, args, logStream, dockerClient, mountDirs, workingDir, envVars, createOptions, }: RunDockerContainerOptions) => Promise<{
+ error: any;
+ statusCode: any;
+}>;
+
+// @public
+export type SearchResponse = {
+ files: SearchResponseFile[];
+ etag: string;
+};
+
+// @public
+export type SearchResponseFile = {
+ url: string;
+ content(): Promise;
+};
+
+// @public (undocumented)
+export type ServiceBuilder = {
+ loadConfig(config: ConfigReader): ServiceBuilder;
+ setPort(port: number): ServiceBuilder;
+ setHost(host: string): ServiceBuilder;
+ setLogger(logger: Logger): ServiceBuilder;
+ enableCors(options: cors.CorsOptions): ServiceBuilder;
+ setHttpsSettings(settings: HttpsSettings): ServiceBuilder;
+ addRouter(root: string, router: Router | RequestHandler): ServiceBuilder;
+ start(): Promise;
+};
+
+// @public (undocumented)
+export function setRootLogger(newLogger: winston.Logger): void;
+
+// @public
+export class SingleConnectionDatabaseManager {
+ forPlugin(pluginId: string): PluginDatabaseManager;
+ static fromConfig(config: Config): SingleConnectionDatabaseManager;
+ }
+
+// @public
+export class SingleHostDiscovery implements PluginEndpointDiscovery {
+ static fromConfig(config: Config, options?: {
+ basePath?: string;
+ }): SingleHostDiscovery;
+ // (undocumented)
+ getBaseUrl(pluginId: string): Promise;
+ // (undocumented)
+ getExternalBaseUrl(pluginId: string): Promise;
+ }
+
+// @public (undocumented)
+export type StatusCheck = () => Promise;
+
+// @public
+export function statusCheckHandler(options?: StatusCheckHandlerOptions): Promise;
+
+// @public (undocumented)
+export interface StatusCheckHandlerOptions {
+ statusCheck?: StatusCheck;
+}
+
+// @public
+export type UrlReader = {
+ read(url: string): Promise;
+ readTree(url: string, options?: ReadTreeOptions): Promise;
+ search(url: string, options?: SearchOptions): Promise;
+};
+
+// @public
+export class UrlReaders {
+ static create({ logger, config, factories }: CreateOptions): UrlReader;
+ static default({ logger, config, factories }: CreateOptions): UrlReader;
+}
+
+// @public
+export function useHotCleanup(_module: NodeModule, cancelEffect: () => void): void;
+
+// @public
+export function useHotMemoize(_module: NodeModule, valueFactory: () => T): T;
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index 41fa657818..998ac475d9 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
- "version": "0.6.2",
+ "version": "0.6.3",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -73,7 +73,7 @@
}
},
"devDependencies": {
- "@backstage/cli": "^0.6.7",
+ "@backstage/cli": "^0.6.9",
"@backstage/test-utils": "^0.1.10",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts
index 07da975184..c17545779a 100644
--- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts
+++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts
@@ -18,7 +18,7 @@ import { Config } from '@backstage/config';
import compression from 'compression';
import cors from 'cors';
import express, { Router } from 'express';
-import helmet, { HelmetOptions } from 'helmet';
+import helmet from 'helmet';
import * as http from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
@@ -195,14 +195,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
});
}
- private getOptions(): {
- port: number;
- host: string;
- logger: Logger;
- corsOptions?: cors.CorsOptions;
- httpsSettings?: HttpsSettings;
- helmetOptions: HelmetOptions;
- } {
+ private getOptions() {
return {
port: this.port ?? DEFAULT_PORT,
host: this.host ?? DEFAULT_HOST,
diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md
index 6585ea2723..ee04e2bb6b 100644
--- a/packages/backend/CHANGELOG.md
+++ b/packages/backend/CHANGELOG.md
@@ -1,5 +1,29 @@
# example-backend
+## 0.2.25
+
+### Patch Changes
+
+- Updated dependencies [b9b2b4b76]
+- Updated dependencies [84c54474d]
+- Updated dependencies [49574a8a3]
+- Updated dependencies [d367f63b5]
+- Updated dependencies [5fe62f124]
+- Updated dependencies [09b5fcf2e]
+- Updated dependencies [55b2fc0c0]
+- Updated dependencies [c42cd1daa]
+- Updated dependencies [b42531cfe]
+- Updated dependencies [c2306f898]
+ - @backstage/plugin-search-backend@0.1.3
+ - @backstage/plugin-search-backend-node@0.1.3
+ - @backstage/plugin-scaffolder-backend@0.10.0
+ - @backstage/plugin-rollbar-backend@0.1.9
+ - @backstage/backend-common@0.6.3
+ - @backstage/plugin-catalog-backend@0.8.0
+ - @backstage/plugin-code-coverage-backend@0.1.2
+ - @backstage/plugin-kubernetes-backend@0.3.5
+ - example-app@0.2.25
+
## 0.2.22
### Patch Changes
diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile
index 02aab45609..31231a3a4a 100644
--- a/packages/backend/Dockerfile
+++ b/packages/backend/Dockerfile
@@ -22,7 +22,7 @@ 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 ./
+COPY 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"]
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 40e5a1cd2f..440ceca8e5 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend",
- "version": "0.2.22",
+ "version": "0.2.25",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -27,29 +27,30 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
- "@backstage/backend-common": "^0.6.1",
+ "@backstage/backend-common": "^0.6.3",
"@backstage/catalog-client": "^0.3.9",
"@backstage/catalog-model": "^0.7.5",
"@backstage/config": "^0.1.4",
"@backstage/plugin-app-backend": "^0.3.10",
"@backstage/plugin-auth-backend": "^0.3.7",
"@backstage/plugin-badges-backend": "^0.1.1",
- "@backstage/plugin-catalog-backend": "^0.7.0",
+ "@backstage/plugin-catalog-backend": "^0.8.0",
+ "@backstage/plugin-code-coverage-backend": "^0.1.2",
"@backstage/plugin-graphql-backend": "^0.1.6",
- "@backstage/plugin-kubernetes-backend": "^0.3.3",
+ "@backstage/plugin-kubernetes-backend": "^0.3.5",
"@backstage/plugin-kafka-backend": "^0.2.3",
"@backstage/plugin-proxy-backend": "^0.2.6",
- "@backstage/plugin-rollbar-backend": "^0.1.8",
- "@backstage/plugin-scaffolder-backend": "^0.9.4",
- "@backstage/plugin-search-backend": "^0.1.2",
- "@backstage/plugin-search-backend-node": "^0.1.2",
+ "@backstage/plugin-rollbar-backend": "^0.1.9",
+ "@backstage/plugin-scaffolder-backend": "^0.10.0",
+ "@backstage/plugin-search-backend": "^0.1.3",
+ "@backstage/plugin-search-backend-node": "^0.1.3",
"@backstage/plugin-techdocs-backend": "^0.7.0",
"@backstage/plugin-todo-backend": "^0.1.3",
"@gitbeaker/node": "^28.0.2",
"@octokit/rest": "^18.0.12",
"azure-devops-node-api": "^10.1.1",
"dockerode": "^3.2.1",
- "example-app": "^0.2.21",
+ "example-app": "^0.2.25",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"knex": "^0.95.1",
@@ -59,7 +60,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
- "@backstage/cli": "^0.6.6",
+ "@backstage/cli": "^0.6.9",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5"
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index cbbdaecf5e..00ed248df5 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -37,6 +37,7 @@ import { Config } from '@backstage/config';
import healthcheck from './plugins/healthcheck';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
+import codeCoverage from './plugins/codecoverage';
import kubernetes from './plugins/kubernetes';
import kafka from './plugins/kafka';
import rollbar from './plugins/rollbar';
@@ -75,6 +76,9 @@ async function main() {
const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
+ const codeCoverageEnv = useHotMemoize(module, () =>
+ createEnv('code-coverage'),
+ );
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
@@ -90,6 +94,7 @@ async function main() {
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
+ apiRouter.use('/code-coverage', await codeCoverage(codeCoverageEnv));
apiRouter.use('/rollbar', await rollbar(rollbarEnv));
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
apiRouter.use('/auth', await auth(authEnv));
diff --git a/packages/backend/src/plugins/codecoverage.ts b/packages/backend/src/plugins/codecoverage.ts
new file mode 100644
index 0000000000..c06e0e516f
--- /dev/null
+++ b/packages/backend/src/plugins/codecoverage.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { createRouter } from '@backstage/plugin-code-coverage-backend';
+import { PluginEnvironment } from '../types';
+
+export default async function createPlugin(env: PluginEnvironment) {
+ return await createRouter({
+ config: env.config,
+ discovery: env.discovery,
+ database: env.database,
+ urlReader: env.reader,
+ logger: env.logger,
+ });
+}
diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts
index 7bd473756d..1691f53485 100644
--- a/packages/backend/src/plugins/search.ts
+++ b/packages/backend/src/plugins/search.ts
@@ -15,7 +15,10 @@
*/
import { useHotCleanup } from '@backstage/backend-common';
import { createRouter } from '@backstage/plugin-search-backend';
-import { IndexBuilder } from '@backstage/plugin-search-backend-node';
+import {
+ IndexBuilder,
+ LunrSearchEngine,
+} from '@backstage/plugin-search-backend-node';
import { PluginEnvironment } from '../types';
import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend';
@@ -23,21 +26,22 @@ export default async function createPlugin({
logger,
discovery,
}: PluginEnvironment) {
- const indexBuilder = new IndexBuilder({ logger });
+ const searchEngine = new LunrSearchEngine({ logger });
+ const indexBuilder = new IndexBuilder({ logger, searchEngine });
indexBuilder.addCollator({
type: 'software-catalog',
defaultRefreshIntervalSeconds: 600,
- collator: new DefaultCatalogCollator(discovery),
+ collator: new DefaultCatalogCollator({ discovery }),
});
- // TODO: Move refresh loop logic into the builder.
- const timerId = setInterval(() => {
- indexBuilder.build();
- }, 60000);
- useHotCleanup(module, () => clearInterval(timerId));
+ const { scheduler } = await indexBuilder.build();
+
+ scheduler.start();
+ useHotCleanup(module, () => scheduler.stop());
return await createRouter({
+ engine: indexBuilder.getSearchEngine(),
logger,
});
}
diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md
new file mode 100644
index 0000000000..4b489ff2e4
--- /dev/null
+++ b/packages/catalog-client/api-report.md
@@ -0,0 +1,82 @@
+## API Report File for "@backstage/catalog-client"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { Entity } from '@backstage/catalog-model';
+import { EntityName } from '@backstage/catalog-model';
+import { Location as Location_2 } from '@backstage/catalog-model';
+
+// @public (undocumented)
+export type AddLocationRequest = {
+ type?: string;
+ target: string;
+ dryRun?: boolean;
+ presence?: 'optional' | 'required';
+};
+
+// @public (undocumented)
+export type AddLocationResponse = {
+ location: Location_2;
+ entities: Entity[];
+};
+
+// @public (undocumented)
+export interface CatalogApi {
+ // (undocumented)
+ addLocation(location: AddLocationRequest, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ getEntities(request?: CatalogEntitiesRequest, options?: CatalogRequestOptions): Promise>;
+ // (undocumented)
+ getEntityByName(name: EntityName, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ getLocationByEntity(entity: Entity, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ getLocationById(id: string, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ getOriginLocationByEntity(entity: Entity, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ removeEntityByUid(uid: string, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ removeLocationById(id: string, options?: CatalogRequestOptions): Promise;
+}
+
+// @public (undocumented)
+export class CatalogClient implements CatalogApi {
+ constructor(options: {
+ discoveryApi: DiscoveryApi;
+ });
+ // (undocumented)
+ addLocation({ type, target, dryRun, presence }: AddLocationRequest, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ getEntities(request?: CatalogEntitiesRequest, options?: CatalogRequestOptions): Promise>;
+ // (undocumented)
+ getEntityByName(compoundName: EntityName, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ getLocationByEntity(entity: Entity, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ getLocationById(id: string, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ getOriginLocationByEntity(entity: Entity, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ removeEntityByUid(uid: string, options?: CatalogRequestOptions): Promise;
+ // (undocumented)
+ removeLocationById(id: string, options?: CatalogRequestOptions): Promise;
+ }
+
+// @public (undocumented)
+export type CatalogEntitiesRequest = {
+ filter?: Record[] | Record | undefined;
+ fields?: string[] | undefined;
+};
+
+// @public (undocumented)
+export type CatalogListResponse = {
+ items: T[];
+};
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts
index 19da45e4ec..359e3a2c60 100644
--- a/packages/catalog-client/src/CatalogClient.test.ts
+++ b/packages/catalog-client/src/CatalogClient.test.ts
@@ -76,7 +76,38 @@ describe('CatalogClient', () => {
expect(response).toEqual(defaultResponse);
});
- it('builds entity search filters properly', async () => {
+ it('builds multiple entity search filters properly', async () => {
+ expect.assertions(2);
+
+ server.use(
+ rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
+ expect(req.url.search).toBe(
+ '?filter=a=1,b=2,b=3,%C3%B6=%3D&filter=a=2',
+ );
+ return res(ctx.json([]));
+ }),
+ );
+
+ const response = await client.getEntities(
+ {
+ filter: [
+ {
+ a: '1',
+ b: ['2', '3'],
+ ö: '=',
+ },
+ {
+ a: '2',
+ },
+ ],
+ },
+ { token },
+ );
+
+ expect(response.items).toEqual([]);
+ });
+
+ it('builds single entity search filter properly', async () => {
expect.assertions(2);
server.use(
diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts
index 3de25e9808..a246b3abab 100644
--- a/packages/catalog-client/src/CatalogClient.ts
+++ b/packages/catalog-client/src/CatalogClient.ts
@@ -56,18 +56,27 @@ export class CatalogClient implements CatalogApi {
request?: CatalogEntitiesRequest,
options?: CatalogRequestOptions,
): Promise> {
- const { filter = {}, fields = [] } = request ?? {};
+ const { filter = [], fields = [] } = request ?? {};
+ const filterItems = [filter].flat();
const params: string[] = [];
- const filterParts: string[] = [];
- for (const [key, value] of Object.entries(filter)) {
- for (const v of [value].flat()) {
- filterParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
+ // filter param can occur multiple times, for example
+ // /api/catalog/entities?filter=metadata.name=wayback-search,kind=component&filter=metadata.name=www-artist,kind=component'
+ // the "outer array" defined by `filter` occurrences corresponds to "anyOf" filters
+ // the "inner array" defined within a `filter` param corresponds to "allOf" filters
+ for (const filterItem of filterItems) {
+ const filterParts: string[] = [];
+ for (const [key, value] of Object.entries(filterItem)) {
+ for (const v of [value].flat()) {
+ filterParts.push(
+ `${encodeURIComponent(key)}=${encodeURIComponent(v)}`,
+ );
+ }
}
- }
- if (filterParts.length) {
- params.push(`filter=${filterParts.join(',')}`);
+ if (filterParts.length) {
+ params.push(`filter=${filterParts.join(',')}`);
+ }
}
if (fields.length) {
diff --git a/packages/catalog-client/src/types.ts b/packages/catalog-client/src/types.ts
index 0d25bf7483..ef907eafa9 100644
--- a/packages/catalog-client/src/types.ts
+++ b/packages/catalog-client/src/types.ts
@@ -17,7 +17,10 @@
import { Entity, EntityName, Location } from '@backstage/catalog-model';
export type CatalogEntitiesRequest = {
- filter?: Record | undefined;
+ filter?:
+ | Record[]
+ | Record
+ | undefined;
fields?: string[] | undefined;
};
diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md
new file mode 100644
index 0000000000..c37c86b01f
--- /dev/null
+++ b/packages/catalog-model/api-report.md
@@ -0,0 +1,571 @@
+## API Report File for "@backstage/catalog-model"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { JsonObject } from '@backstage/config';
+import { JSONSchema7 } from 'json-schema';
+import { JsonValue } from '@backstage/config';
+import * as yup from 'yup';
+
+// @public (undocumented)
+export const analyzeLocationSchema: yup.ObjectSchema<{
+ location: LocationSpec;
+}, object>;
+
+// @public (undocumented)
+interface ApiEntityV1alpha1 extends Entity {
+ // (undocumented)
+ apiVersion: typeof API_VERSION[number];
+ // (undocumented)
+ kind: typeof KIND;
+ // (undocumented)
+ spec: {
+ type: string;
+ lifecycle: string;
+ owner: string;
+ definition: string;
+ system?: string;
+ };
+}
+
+export { ApiEntityV1alpha1 as ApiEntity }
+
+export { ApiEntityV1alpha1 }
+
+// @public (undocumented)
+export const apiEntityV1alpha1Validator: KindValidator;
+
+// @public
+export class CommonValidatorFunctions {
+ static isJsonSafe(value: unknown): boolean;
+ static isValidDnsLabel(value: unknown): boolean;
+ static isValidDnsSubdomain(value: unknown): boolean;
+ static isValidPrefixAndOrSuffix(value: unknown, separator: string, isValidPrefix: (value: string) => boolean, isValidSuffix: (value: string) => boolean): boolean;
+ static isValidString(value: unknown): boolean;
+ static isValidUrl(value: unknown): boolean;
+}
+
+// @public
+export function compareEntityToRef(entity: Entity, ref: EntityRef | EntityName, context?: EntityRefContext): boolean;
+
+// @public (undocumented)
+interface ComponentEntityV1alpha1 extends Entity {
+ // (undocumented)
+ apiVersion: typeof API_VERSION_2[number];
+ // (undocumented)
+ kind: typeof KIND_2;
+ // (undocumented)
+ spec: {
+ type: string;
+ lifecycle: string;
+ owner: string;
+ subcomponentOf?: string;
+ providesApis?: string[];
+ consumesApis?: string[];
+ dependsOn?: string[];
+ system?: string;
+ };
+}
+
+export { ComponentEntityV1alpha1 as ComponentEntity }
+
+export { ComponentEntityV1alpha1 }
+
+// @public (undocumented)
+export const componentEntityV1alpha1Validator: KindValidator;
+
+// @public
+export class DefaultNamespaceEntityPolicy implements EntityPolicy {
+ constructor(namespace?: string);
+ // (undocumented)
+ enforce(entity: Entity): Promise;
+ }
+
+// @public (undocumented)
+interface DomainEntityV1alpha1 extends Entity {
+ // (undocumented)
+ apiVersion: typeof API_VERSION_3[number];
+ // (undocumented)
+ kind: typeof KIND_3;
+ // (undocumented)
+ spec: {
+ owner: string;
+ };
+}
+
+export { DomainEntityV1alpha1 as DomainEntity }
+
+export { DomainEntityV1alpha1 }
+
+// @public (undocumented)
+export const domainEntityV1alpha1Validator: KindValidator;
+
+// @public (undocumented)
+export const EDIT_URL_ANNOTATION = "backstage.io/edit-url";
+
+// @public
+export type Entity = {
+ apiVersion: string;
+ kind: string;
+ metadata: EntityMeta;
+ spec?: JsonObject;
+ relations?: EntityRelation[];
+};
+
+// @public
+export const ENTITY_DEFAULT_NAMESPACE = "default";
+
+// @public
+export const ENTITY_META_GENERATED_FIELDS: readonly ["uid", "etag", "generation"];
+
+// @public
+export function entityHasChanges(previous: Entity, next: Entity): boolean;
+
+// @public
+export type EntityLink = {
+ url: string;
+ title?: string;
+ icon?: string;
+};
+
+// @public
+export type EntityMeta = JsonObject & {
+ uid?: string;
+ etag?: string;
+ generation?: number;
+ name: string;
+ namespace?: string;
+ description?: string;
+ labels?: Record;
+ annotations?: Record;
+ tags?: string[];
+ links?: EntityLink[];
+};
+
+// @public
+export type EntityName = {
+ kind: string;
+ namespace: string;
+ name: string;
+};
+
+// @public (undocumented)
+export const EntityPolicies: {
+ allOf(policies: EntityPolicy[]): AllEntityPolicies;
+ oneOf(policies: EntityPolicy[]): AnyEntityPolicy;
+};
+
+// @public
+export type EntityPolicy = {
+ enforce(entity: Entity): Promise;
+};
+
+// @public
+export type EntityRef = string | {
+ kind?: string;
+ namespace?: string;
+ name: string;
+};
+
+// @public
+export type EntityRelation = {
+ type: string;
+ target: EntityName;
+};
+
+// @public
+export type EntityRelationSpec = {
+ source: EntityName;
+ type: string;
+ target: EntityName;
+};
+
+// @public
+export class FieldFormatEntityPolicy implements EntityPolicy {
+ constructor(validators?: Validators);
+ // (undocumented)
+ enforce(entity: Entity): Promise;
+ }
+
+// @public
+export function generateEntityEtag(): string;
+
+// @public
+export function generateEntityUid(): string;
+
+// @public
+export function generateUpdatedEntity(previous: Entity, next: Entity): Entity;
+
+// @public
+export function getEntityName(entity: Entity): EntityName;
+
+// @public
+export function getEntitySourceLocation(entity: Entity): {
+ type: string;
+ target: string;
+};
+
+// @public (undocumented)
+interface GroupEntityV1alpha1 extends Entity {
+ // (undocumented)
+ apiVersion: typeof API_VERSION_4[number];
+ // (undocumented)
+ kind: typeof KIND_4;
+ // (undocumented)
+ spec: {
+ type: string;
+ profile?: {
+ displayName?: string;
+ email?: string;
+ picture?: string;
+ };
+ parent?: string;
+ children: string[];
+ members?: string[];
+ };
+}
+
+export { GroupEntityV1alpha1 as GroupEntity }
+
+export { GroupEntityV1alpha1 }
+
+// @public (undocumented)
+export const groupEntityV1alpha1Validator: KindValidator;
+
+// @public (undocumented)
+export type JSONSchema = JSONSchema7 & {
+ [key in string]?: JsonValue;
+};
+
+// @public
+export type KindValidator = {
+ check(entity: Entity): Promise;
+};
+
+// @public
+export class KubernetesValidatorFunctions {
+ // (undocumented)
+ static isValidAnnotationKey(value: unknown): boolean;
+ // (undocumented)
+ static isValidAnnotationValue(value: unknown): boolean;
+ // (undocumented)
+ static isValidApiVersion(value: unknown): boolean;
+ // (undocumented)
+ static isValidKind(value: unknown): boolean;
+ // (undocumented)
+ static isValidLabelKey(value: unknown): boolean;
+ // (undocumented)
+ static isValidLabelValue(value: unknown): boolean;
+ // (undocumented)
+ static isValidNamespace(value: unknown): boolean;
+ // (undocumented)
+ static isValidObjectName(value: unknown): boolean;
+}
+
+// @public (undocumented)
+type Location_2 = {
+ id: string;
+} & LocationSpec;
+
+export { Location_2 as Location }
+
+// @public (undocumented)
+export const LOCATION_ANNOTATION = "backstage.io/managed-by-location";
+
+// @public (undocumented)
+interface LocationEntityV1alpha1 extends Entity {
+ // (undocumented)
+ apiVersion: typeof API_VERSION_5[number];
+ // (undocumented)
+ kind: typeof KIND_5;
+ // (undocumented)
+ spec: {
+ type?: string;
+ target?: string;
+ targets?: string[];
+ };
+}
+
+export { LocationEntityV1alpha1 as LocationEntity }
+
+export { LocationEntityV1alpha1 }
+
+// @public (undocumented)
+export const locationEntityV1alpha1Validator: KindValidator;
+
+// @public (undocumented)
+export const locationSchema: yup.ObjectSchema;
+
+// @public (undocumented)
+export type LocationSpec = {
+ type: string;
+ target: string;
+ presence?: 'optional' | 'required';
+};
+
+// @public (undocumented)
+export const locationSpecSchema: yup.ObjectSchema;
+
+// @public (undocumented)
+export function makeValidator(overrides?: Partial): Validators;
+
+// @public
+export class NoForeignRootFieldsEntityPolicy implements EntityPolicy {
+ constructor(knownFields?: string[]);
+ // (undocumented)
+ enforce(entity: Entity): Promise;
+ }
+
+// @public (undocumented)
+export const ORIGIN_LOCATION_ANNOTATION = "backstage.io/managed-by-origin-location";
+
+// @public
+export function parseEntityName(ref: EntityRef, context?: EntityRefContext): EntityName;
+
+// @public
+export function parseEntityRef(ref: EntityRef, context?: {
+ defaultKind: string;
+ defaultNamespace: string;
+}): {
+ kind: string;
+ namespace: string;
+ name: string;
+};
+
+// @public (undocumented)
+export function parseEntityRef(ref: EntityRef, context?: {
+ defaultKind: string;
+}): {
+ kind: string;
+ namespace?: string;
+ name: string;
+};
+
+// @public (undocumented)
+export function parseEntityRef(ref: EntityRef, context?: {
+ defaultNamespace: string;
+}): {
+ kind?: string;
+ namespace: string;
+ name: string;
+};
+
+// @public
+export function parseLocationReference(ref: string): {
+ type: string;
+ target: string;
+};
+
+// @public (undocumented)
+export const RELATION_API_CONSUMED_BY = "apiConsumedBy";
+
+// @public (undocumented)
+export const RELATION_API_PROVIDED_BY = "apiProvidedBy";
+
+// @public (undocumented)
+export const RELATION_CHILD_OF = "childOf";
+
+// @public
+export const RELATION_CONSUMES_API = "consumesApi";
+
+// @public (undocumented)
+export const RELATION_DEPENDENCY_OF = "dependencyOf";
+
+// @public
+export const RELATION_DEPENDS_ON = "dependsOn";
+
+// @public (undocumented)
+export const RELATION_HAS_MEMBER = "hasMember";
+
+// @public (undocumented)
+export const RELATION_HAS_PART = "hasPart";
+
+// @public
+export const RELATION_MEMBER_OF = "memberOf";
+
+// @public
+export const RELATION_OWNED_BY = "ownedBy";
+
+// @public (undocumented)
+export const RELATION_OWNER_OF = "ownerOf";
+
+// @public
+export const RELATION_PARENT_OF = "parentOf";
+
+// @public
+export const RELATION_PART_OF = "partOf";
+
+// @public (undocumented)
+export const RELATION_PROVIDES_API = "providesApi";
+
+// @public (undocumented)
+interface ResourceEntityV1alpha1 extends Entity {
+ // (undocumented)
+ apiVersion: typeof API_VERSION_6[number];
+ // (undocumented)
+ kind: typeof KIND_6;
+ // (undocumented)
+ spec: {
+ type: string;
+ owner: string;
+ dependsOn?: string[];
+ system?: string;
+ };
+}
+
+export { ResourceEntityV1alpha1 as ResourceEntity }
+
+export { ResourceEntityV1alpha1 }
+
+// @public (undocumented)
+export const resourceEntityV1alpha1Validator: KindValidator;
+
+// @public @deprecated (undocumented)
+export function schemaValidator(kind: string, apiVersion: readonly string[], schema: yup.Schema): KindValidator;
+
+// @public
+export class SchemaValidEntityPolicy implements EntityPolicy {
+ // (undocumented)
+ enforce(entity: Entity): Promise;
+ }
+
+// @public @deprecated
+export function serializeEntityRef(ref: Entity | {
+ kind?: string;
+ namespace?: string;
+ name: string;
+}): EntityRef;
+
+// @public (undocumented)
+export const SOURCE_LOCATION_ANNOTATION = "backstage.io/source-location";
+
+// @public
+export function stringifyEntityRef(ref: Entity | {
+ kind: string;
+ namespace?: string;
+ name: string;
+}): string;
+
+// @public
+export function stringifyLocationReference(ref: {
+ type: string;
+ target: string;
+}): string;
+
+// @public (undocumented)
+interface SystemEntityV1alpha1 extends Entity {
+ // (undocumented)
+ apiVersion: typeof API_VERSION_7[number];
+ // (undocumented)
+ kind: typeof KIND_7;
+ // (undocumented)
+ spec: {
+ owner: string;
+ domain?: string;
+ };
+}
+
+export { SystemEntityV1alpha1 as SystemEntity }
+
+export { SystemEntityV1alpha1 }
+
+// @public (undocumented)
+export const systemEntityV1alpha1Validator: KindValidator;
+
+// @public (undocumented)
+interface TemplateEntityV1alpha1 extends Entity {
+ // (undocumented)
+ apiVersion: typeof API_VERSION_8[number];
+ // (undocumented)
+ kind: typeof KIND_8;
+ // (undocumented)
+ spec: {
+ type: string;
+ templater: string;
+ path?: string;
+ schema: JSONSchema;
+ };
+}
+
+export { TemplateEntityV1alpha1 as TemplateEntity }
+
+export { TemplateEntityV1alpha1 }
+
+// @public (undocumented)
+export const templateEntityV1alpha1Validator: KindValidator;
+
+// @public (undocumented)
+export interface TemplateEntityV1beta2 extends Entity {
+ // (undocumented)
+ apiVersion: typeof API_VERSION_9[number];
+ // (undocumented)
+ kind: typeof KIND_9;
+ // (undocumented)
+ metadata: EntityMeta & {
+ title?: string;
+ };
+ // (undocumented)
+ spec: {
+ type: string;
+ parameters?: JsonObject | JsonObject[];
+ steps: Array<{
+ id?: string;
+ name?: string;
+ action: string;
+ parameters?: JsonObject;
+ }>;
+ output?: {
+ [name: string]: string;
+ };
+ };
+}
+
+// @public (undocumented)
+export const templateEntityV1beta2Validator: KindValidator;
+
+// @public (undocumented)
+interface UserEntityV1alpha1 extends Entity {
+ // (undocumented)
+ apiVersion: typeof API_VERSION_10[number];
+ // (undocumented)
+ kind: typeof KIND_10;
+ // (undocumented)
+ spec: {
+ profile?: {
+ displayName?: string;
+ email?: string;
+ picture?: string;
+ };
+ memberOf: string[];
+ };
+}
+
+export { UserEntityV1alpha1 as UserEntity }
+
+export { UserEntityV1alpha1 }
+
+// @public (undocumented)
+export const userEntityV1alpha1Validator: KindValidator;
+
+// @public (undocumented)
+export type Validators = {
+ isValidApiVersion(value: unknown): boolean;
+ isValidKind(value: unknown): boolean;
+ isValidEntityName(value: unknown): boolean;
+ isValidNamespace(value: unknown): boolean;
+ isValidLabelKey(value: unknown): boolean;
+ isValidLabelValue(value: unknown): boolean;
+ isValidAnnotationKey(value: unknown): boolean;
+ isValidAnnotationValue(value: unknown): boolean;
+ isValidTag(value: unknown): boolean;
+};
+
+// @public
+export const VIEW_URL_ANNOTATION = "backstage.io/view-url";
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json
index 64550b35d1..941877aec5 100644
--- a/packages/catalog-model/package.json
+++ b/packages/catalog-model/package.json
@@ -33,7 +33,7 @@
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.29.8",
"ajv": "^7.0.3",
- "json-schema": "^0.2.5",
+ "json-schema": "^0.3.0",
"lodash": "^4.17.15",
"uuid": "^8.0.0",
"yup": "^0.29.3"
diff --git a/packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json
index 5bb83d116b..53109ac7ee 100644
--- a/packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json
+++ b/packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json
@@ -66,7 +66,7 @@
"properties": {
"type": {
"type": "string",
- "description": "The type of component. This field is optional but recommended. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.",
+ "description": "The type of component created by the template. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.",
"examples": ["service", "website", "library"],
"minLength": 1
},
diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json
index ba3ccb9d1d..1e0b7d0ded 100644
--- a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json
+++ b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json
@@ -85,7 +85,7 @@
"properties": {
"type": {
"type": "string",
- "description": "The type of component. This field is optional but recommended. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.",
+ "description": "The type of component created by the template. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.",
"examples": ["service", "website", "library"],
"minLength": 1
},
diff --git a/packages/cli-common/api-report.md b/packages/cli-common/api-report.md
new file mode 100644
index 0000000000..bcdc023c22
--- /dev/null
+++ b/packages/cli-common/api-report.md
@@ -0,0 +1,25 @@
+## API Report File for "@backstage/cli-common"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+// @public
+export function findPaths(searchDir: string): Paths;
+
+// @public (undocumented)
+export type Paths = {
+ ownDir: string;
+ ownRoot: string;
+ targetDir: string;
+ targetRoot: string;
+ resolveOwn: ResolveFunc;
+ resolveOwnRoot: ResolveFunc;
+ resolveTarget: ResolveFunc;
+ resolveTargetRoot: ResolveFunc;
+};
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index a1381495ff..af452b3c9d 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/cli
+## 0.6.9
+
+### Patch Changes
+
+- 4e5c94249: Add `config:docs` command that opens up reference documentation for the local configuration schema in a browser.
+- 1373f4f12: No longer add newly created plugins to `plugins.ts` in the app, as it is no longer needed.
+- 479b29124: Added support for Datadog rum events
+
## 0.6.8
### Patch Changes
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 251b2ab076..1110efefe6 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
- "version": "0.6.8",
+ "version": "0.6.9",
"private": false,
"publishConfig": {
"access": "public"
@@ -82,7 +82,7 @@
"inquirer": "^7.0.4",
"jest": "^26.0.1",
"jest-css-modules": "^2.1.0",
- "json-schema": "^0.2.5",
+ "json-schema": "^0.3.0",
"lodash": "^4.17.19",
"mini-css-extract-plugin": "^0.9.0",
"ora": "^5.3.0",
@@ -116,12 +116,12 @@
"yn": "^4.0.0"
},
"devDependencies": {
- "@backstage/backend-common": "^0.6.2",
+ "@backstage/backend-common": "^0.6.3",
"@backstage/config": "^0.1.4",
- "@backstage/core": "^0.7.5",
+ "@backstage/core": "^0.7.6",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
- "@backstage/theme": "^0.2.5",
+ "@backstage/theme": "^0.2.6",
"@types/diff": "^5.0.0",
"@types/express": "^4.17.6",
"@types/fs-extra": "^9.0.1",
diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md
new file mode 100644
index 0000000000..a09311b786
--- /dev/null
+++ b/packages/config-loader/api-report.md
@@ -0,0 +1,45 @@
+## API Report File for "@backstage/config-loader"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { AppConfig } from '@backstage/config';
+import { JsonObject } from '@backstage/config';
+import { JSONSchema7 } from 'json-schema';
+
+// @public
+export type ConfigSchema = {
+ process(appConfigs: AppConfig[], options?: ConfigProcessingOptions): AppConfig[];
+ serialize(): JsonObject;
+};
+
+// @public
+export type ConfigVisibility = typeof CONFIG_VISIBILITIES[number];
+
+// @public (undocumented)
+export function loadConfig(options: LoadConfigOptions): Promise;
+
+// @public (undocumented)
+export type LoadConfigOptions = {
+ configRoot: string;
+ configPaths: string[];
+ env?: string;
+ experimentalEnvFunc?: EnvFunc;
+};
+
+// @public
+export function loadConfigSchema(options: Options): Promise;
+
+// @public
+export function mergeConfigSchemas(schemas: JSONSchema7[]): JSONSchema7;
+
+// @public
+export function readEnvConfig(env: {
+ [name: string]: string | undefined;
+}): AppConfig[];
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json
index 0a0a76c2d6..55d86314af 100644
--- a/packages/config-loader/package.json
+++ b/packages/config-loader/package.json
@@ -35,7 +35,7 @@
"@types/json-schema": "^7.0.6",
"ajv": "^7.0.3",
"fs-extra": "^9.0.0",
- "json-schema": "^0.2.5",
+ "json-schema": "^0.3.0",
"json-schema-merge-allof": "^0.7.0",
"typescript-json-schema": "^0.49.0",
"yaml": "^1.9.2",
diff --git a/packages/config/api-report.md b/packages/config/api-report.md
new file mode 100644
index 0000000000..be250778ec
--- /dev/null
+++ b/packages/config/api-report.md
@@ -0,0 +1,90 @@
+## API Report File for "@backstage/config"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+// @public (undocumented)
+export type AppConfig = {
+ context: string;
+ data: JsonObject;
+};
+
+// @public (undocumented)
+export type Config = {
+ has(key: string): boolean;
+ keys(): string[];
+ get(key?: string): T;
+ getOptional(key?: string): T | undefined;
+ getConfig(key: string): Config;
+ getOptionalConfig(key: string): Config | undefined;
+ getConfigArray(key: string): Config[];
+ getOptionalConfigArray(key: string): Config[] | undefined;
+ getNumber(key: string): number;
+ getOptionalNumber(key: string): number | undefined;
+ getBoolean(key: string): boolean;
+ getOptionalBoolean(key: string): boolean | undefined;
+ getString(key: string): string;
+ getOptionalString(key: string): string | undefined;
+ getStringArray(key: string): string[];
+ getOptionalStringArray(key: string): string[] | undefined;
+};
+
+// @public (undocumented)
+export class ConfigReader implements Config {
+ constructor(data: JsonObject | undefined, context?: string, fallback?: ConfigReader | undefined, prefix?: string);
+ // (undocumented)
+ static fromConfigs(configs: AppConfig[]): ConfigReader;
+ // (undocumented)
+ get(key?: string): T;
+ // (undocumented)
+ getBoolean(key: string): boolean;
+ // (undocumented)
+ getConfig(key: string): ConfigReader;
+ // (undocumented)
+ getConfigArray(key: string): ConfigReader[];
+ // (undocumented)
+ getNumber(key: string): number;
+ // (undocumented)
+ getOptional(key?: string): T | undefined;
+ // (undocumented)
+ getOptionalBoolean(key: string): boolean | undefined;
+ // (undocumented)
+ getOptionalConfig(key: string): ConfigReader | undefined;
+ // (undocumented)
+ getOptionalConfigArray(key: string): ConfigReader[] | undefined;
+ // (undocumented)
+ getOptionalNumber(key: string): number | undefined;
+ // (undocumented)
+ getOptionalString(key: string): string | undefined;
+ // (undocumented)
+ getOptionalStringArray(key: string): string[] | undefined;
+ // (undocumented)
+ getString(key: string): string;
+ // (undocumented)
+ getStringArray(key: string): string[];
+ // (undocumented)
+ has(key: string): boolean;
+ // (undocumented)
+ keys(): string[];
+ }
+
+// @public (undocumented)
+export interface JsonArray extends Array {
+}
+
+// @public (undocumented)
+export type JsonObject = {
+ [key in string]?: JsonValue;
+};
+
+// @public (undocumented)
+export type JsonPrimitive = number | string | boolean | null;
+
+// @public (undocumented)
+export type JsonValue = JsonObject | JsonArray | JsonPrimitive;
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/core-api/CHANGELOG.md b/packages/core-api/CHANGELOG.md
index 609940b8db..fab82a77e5 100644
--- a/packages/core-api/CHANGELOG.md
+++ b/packages/core-api/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/core-api
+## 0.2.17
+
+### Patch Changes
+
+- ab07d77f6: Add support for discovering plugins through the app element tree, removing the need to register them explicitly.
+- 50ce875a0: Fixed a potentially confusing error being thrown about misuse of routable extensions where the error was actually something different.
+- Updated dependencies [931b21a12]
+ - @backstage/theme@0.2.6
+
## 0.2.16
### Patch Changes
diff --git a/packages/core-api/package.json b/packages/core-api/package.json
index 33e5ac146d..a55df67343 100644
--- a/packages/core-api/package.json
+++ b/packages/core-api/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
- "version": "0.2.16",
+ "version": "0.2.17",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.4",
- "@backstage/theme": "^0.2.5",
+ "@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@types/react": "^16.9",
@@ -42,7 +42,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
- "@backstage/cli": "^0.6.7",
+ "@backstage/cli": "^0.6.9",
"@backstage/test-utils": "^0.1.10",
"@backstage/test-utils-core": "^0.1.1",
"@testing-library/jest-dom": "^5.10.1",
diff --git a/packages/core-api/src/extensions/extensions.tsx b/packages/core-api/src/extensions/extensions.tsx
index 8ff4055e61..9f6412f960 100644
--- a/packages/core-api/src/extensions/extensions.tsx
+++ b/packages/core-api/src/extensions/extensions.tsx
@@ -47,12 +47,15 @@ export function createRoutableExtension<
// Validate that the routing is wired up correctly in the App.tsx
try {
useRouteRef(mountPoint);
- } catch {
- throw new Error(
- `Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` +
- 'Routable extension components may not be rendered by other components and must be ' +
- 'directly available as an element within the App provider component.',
- );
+ } catch (error) {
+ if (error?.message.startsWith('No path for ')) {
+ throw new Error(
+ `Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` +
+ 'Routable extension components may not be rendered by other components and must be ' +
+ 'directly available as an element within the App provider component.',
+ );
+ }
+ throw error;
}
return ;
};
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index 7f691a2787..974dcd3b91 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -1,5 +1,30 @@
# @backstage/core
+## 0.7.7
+
+### Patch Changes
+
+- 9afcac5af: Allow passing NavLinkProps to SidebarItem component to use in NavLink
+- e0c9ed759: Add `if` prop to `EntityLayout.Route` to conditionally render tabs
+- 6eaecbd81: Improve owner example value in `MissingAnnotationEmptyState`.
+
+## 0.7.6
+
+### Patch Changes
+
+- 94da20976: Sort the table filter options by name.
+- d8cc7e67a: Exposing Material UI extension point for tabs to be able to add additional information to them
+- 99fbef232: Adding Headings for Accessibility on the Scaffolder Plugin
+- ab07d77f6: Add support for discovering plugins through the app element tree, removing the need to register them explicitly.
+- 937ed39ce: Exported SignInProviderConfig to strongly type SignInPage providers
+- 9a9e7a42f: Adding close button on support menu
+- 50ce875a0: Fixed a potentially confusing error being thrown about misuse of routable extensions where the error was actually something different.
+- Updated dependencies [ab07d77f6]
+- Updated dependencies [931b21a12]
+- Updated dependencies [50ce875a0]
+ - @backstage/core-api@0.2.17
+ - @backstage/theme@0.2.6
+
## 0.7.5
### Patch Changes
diff --git a/packages/core/package.json b/packages/core/package.json
index 7b1898a19d..59391e122a 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
- "version": "0.7.5",
+ "version": "0.7.7",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,9 +30,9 @@
},
"dependencies": {
"@backstage/config": "^0.1.4",
- "@backstage/core-api": "^0.2.16",
+ "@backstage/core-api": "^0.2.17",
"@backstage/errors": "^0.1.1",
- "@backstage/theme": "^0.2.5",
+ "@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -69,7 +69,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
- "@backstage/cli": "^0.6.8",
+ "@backstage/cli": "^0.6.9",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
diff --git a/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx b/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx
index 377373a06d..6c04b34a90 100644
--- a/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx
+++ b/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx
@@ -30,7 +30,7 @@ metadata:
spec:
type: website
lifecycle: production
- owner: guest`;
+ owner: user:guest`;
type Props = {
annotation: string;
diff --git a/packages/core/src/components/TabbedLayout/RoutedTabs.tsx b/packages/core/src/components/TabbedLayout/RoutedTabs.tsx
index b17c8afb3b..6b7b39329b 100644
--- a/packages/core/src/components/TabbedLayout/RoutedTabs.tsx
+++ b/packages/core/src/components/TabbedLayout/RoutedTabs.tsx
@@ -48,7 +48,12 @@ export const RoutedTabs = ({ routes }: { routes: SubRoute[] }) => {
const navigate = useNavigate();
const { index, route, element } = useSelectedSubRoute(routes);
const headerTabs = useMemo(
- () => routes.map(t => ({ id: t.path, label: t.title })),
+ () =>
+ routes.map(t => ({
+ id: t.path,
+ label: t.title,
+ tabProps: t.tabProps,
+ })),
[routes],
);
diff --git a/packages/core/src/components/TabbedLayout/TabbedLayout.tsx b/packages/core/src/components/TabbedLayout/TabbedLayout.tsx
index f181ee5980..5a1b190d00 100644
--- a/packages/core/src/components/TabbedLayout/TabbedLayout.tsx
+++ b/packages/core/src/components/TabbedLayout/TabbedLayout.tsx
@@ -23,11 +23,13 @@ import React, {
ReactNode,
} from 'react';
import { RoutedTabs } from './RoutedTabs';
+import { TabProps } from '@material-ui/core';
type SubRoute = {
path: string;
title: string;
children: JSX.Element;
+ tabProps?: TabProps;
};
const Route: (props: SubRoute) => null = () => null;
@@ -60,8 +62,8 @@ export function createSubRoutesFromChildren(
throw new Error('Child of TabbedLayout must be an TabbedLayout.Route');
}
- const { path, title, children } = child.props;
- return [{ path, title, children }];
+ const { path, title, children, tabProps } = child.props;
+ return [{ path, title, children, tabProps }];
});
}
diff --git a/packages/core/src/components/TabbedLayout/index.ts b/packages/core/src/components/TabbedLayout/index.ts
index 744b56959e..fe72b199ec 100644
--- a/packages/core/src/components/TabbedLayout/index.ts
+++ b/packages/core/src/components/TabbedLayout/index.ts
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export { TabbedLayout } from './TabbedLayout';
+export { RoutedTabs } from './RoutedTabs';
diff --git a/packages/core/src/components/TabbedLayout/types.ts b/packages/core/src/components/TabbedLayout/types.ts
index 29ade88dc0..24ee011933 100644
--- a/packages/core/src/components/TabbedLayout/types.ts
+++ b/packages/core/src/components/TabbedLayout/types.ts
@@ -14,8 +14,12 @@
* limitations under the License.
*/
+import { TabProps } from '@material-ui/core';
+import * as React from 'react';
+
export type SubRoute = {
path: string;
title: string;
children: JSX.Element;
+ tabProps?: TabProps;
};
diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx
index d061be6d60..2251b8304e 100644
--- a/packages/core/src/components/Table/Table.tsx
+++ b/packages/core/src/components/Table/Table.tsx
@@ -363,7 +363,7 @@ export function Table({
placeholder: 'All results',
label: filter.column,
multiple: filter.type === 'multiple-select',
- items: [...extractDistinctValues(filter.column)].map(value => ({
+ items: [...extractDistinctValues(filter.column)].sort().map(value => ({
label: value,
value,
})),
diff --git a/packages/core/src/layout/HeaderTabs/HeaderTabs.test.tsx b/packages/core/src/layout/HeaderTabs/HeaderTabs.test.tsx
index 81d2123049..9a5093f6e9 100644
--- a/packages/core/src/layout/HeaderTabs/HeaderTabs.test.tsx
+++ b/packages/core/src/layout/HeaderTabs/HeaderTabs.test.tsx
@@ -17,6 +17,7 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { HeaderTabs } from './';
+import { Badge, makeStyles } from '@material-ui/core';
const mockTabs = [
{ id: 'overview', label: 'Overview' },
@@ -46,4 +47,35 @@ describe('', () => {
'true',
);
});
+ it('should render extension component to tab if one present', async () => {
+ const useStyles = makeStyles(() => ({
+ badge: {
+ margin: '20px 20px 0 0',
+ },
+ }));
+
+ const TextualBadge = React.forwardRef((props, ref) => (
+
+
+ {props.children}
+
+
+ ));
+ const iconTab = [
+ {
+ id: 'icon-tab',
+ label: 'Alarms',
+ tabProps: { component: TextualBadge },
+ },
+ ];
+
+ const rendered = await renderInTestApp();
+
+ expect(rendered.getByText('Alarms')).toBeInTheDocument();
+ expect(rendered.getByText('three new alarms')).toBeInTheDocument();
+ });
});
diff --git a/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx b/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx
index 718a103f00..1fd4a18ba3 100644
--- a/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx
+++ b/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx
@@ -18,7 +18,7 @@
// This is just a temporary solution to implementing tabs for now
import React, { useState, useEffect } from 'react';
-import { makeStyles, Tabs, Tab as TabUI } from '@material-ui/core';
+import { makeStyles, Tabs, Tab as TabUI, TabProps } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
tabsWrapper: {
@@ -41,6 +41,7 @@ const useStyles = makeStyles(theme => ({
export type Tab = {
id: string;
label: string;
+ tabProps?: TabProps;
};
type HeaderTabsProps = {
@@ -82,6 +83,7 @@ export const HeaderTabs = ({
>
{tabs.map((tab, index) => (
(theme => {
@@ -138,7 +138,7 @@ type SidebarItemButtonProps = SidebarItemBaseProps & {
type SidebarItemLinkProps = SidebarItemBaseProps & {
to: string;
onClick?: (ev: React.MouseEvent) => void;
-};
+} & NavLinkProps;
type SidebarItemProps = SidebarItemButtonProps | SidebarItemLinkProps;
@@ -156,6 +156,7 @@ export const SidebarItem = forwardRef((props, ref) => {
onClick,
children,
className,
+ ...navLinkProps
} = props;
const classes = useStyles();
// XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component
@@ -216,6 +217,7 @@ export const SidebarItem = forwardRef((props, ref) => {
activeClassName={classes.selected}
to={props.to}
ref={ref}
+ {...navLinkProps}
>
{content}
diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md
index 5975b78e06..430e7f003a 100644
--- a/packages/create-app/CHANGELOG.md
+++ b/packages/create-app/CHANGELOG.md
@@ -1,5 +1,96 @@
# @backstage/create-app
+## 0.3.20
+
+### Patch Changes
+
+- 73f3f5d78: Updates the end to end test in the app to match the new catalog index page title. To apply this change to an existing app, update `packages/app/cypress/integration/app.js` to search for `"My Company Catalog"` instead of `"My Company Service Catalog"`.
+- Updated dependencies [1ce80ff02]
+- Updated dependencies [4c42ecca2]
+- Updated dependencies [c614ede9a]
+- Updated dependencies [9afcac5af]
+- Updated dependencies [07a7806c3]
+- Updated dependencies [f6efa71ee]
+- Updated dependencies [19a4dd710]
+- Updated dependencies [a99e0bc42]
+- Updated dependencies [dcd54c7cd]
+- Updated dependencies [da546ce00]
+- Updated dependencies [e0c9ed759]
+- Updated dependencies [6fbd7beca]
+- Updated dependencies [15cbe6815]
+- Updated dependencies [39bdaa004]
+- Updated dependencies [cb8c848a3]
+- Updated dependencies [21fddf452]
+- Updated dependencies [17915e29b]
+- Updated dependencies [a1783f306]
+- Updated dependencies [6eaecbd81]
+- Updated dependencies [23769512a]
+- Updated dependencies [1a142ae8a]
+ - @backstage/plugin-api-docs@0.4.12
+ - @backstage/plugin-github-actions@0.4.4
+ - @backstage/plugin-catalog-import@0.5.4
+ - @backstage/plugin-explore@0.3.4
+ - @backstage/plugin-lighthouse@0.2.15
+ - @backstage/core@0.7.7
+ - @backstage/plugin-scaffolder@0.9.2
+ - @backstage/plugin-catalog@0.5.6
+ - @backstage/plugin-catalog-backend@0.8.1
+ - @backstage/plugin-search@0.3.5
+ - @backstage/plugin-techdocs@0.9.0
+ - @backstage/plugin-scaffolder-backend@0.10.1
+
+## 0.3.19
+
+### Patch Changes
+
+- ee22773e9: Removed `plugins.ts` from the app, as plugins are now discovered through the react tree.
+
+ To apply this change to an existing app, simply delete `packages/app/src/plugins.ts` along with the import and usage in `packages/app/src/App.tsx`.
+
+ Note that there are a few plugins that require explicit registration, in which case you would need to keep them in `plugins.ts`. The set of plugins that need explicit registration is any plugin that doesn't have a component extension that gets rendered as part of the app element tree. An example of such a plugin in the main Backstage repo is `@backstage/plugin-badges`. In the case of the badges plugin this is because there is not yet a component-based API for adding context menu items to the entity layout.
+
+ If you have plugins that still rely on route registration through the `register` method of `createPlugin`, these need to be kept in `plugins.ts` as well. However, it is recommended to migrate these to export an extensions component instead.
+
+- 670acd88e: Fix system diagram card to be on the system page
+
+ To apply the same fix to an existing application, in `EntityPage.tsx` simply move the `` for the `/diagram` path from the `groupPage` down into the `systemPage` element.
+
+- Updated dependencies [94da20976]
+- Updated dependencies [84c54474d]
+- Updated dependencies [d8cc7e67a]
+- Updated dependencies [4e5c94249]
+- Updated dependencies [99fbef232]
+- Updated dependencies [cb0206b2b]
+- Updated dependencies [1373f4f12]
+- Updated dependencies [29a7e4be8]
+- Updated dependencies [ab07d77f6]
+- Updated dependencies [49574a8a3]
+- Updated dependencies [d367f63b5]
+- Updated dependencies [96728a2af]
+- Updated dependencies [5fe62f124]
+- Updated dependencies [931b21a12]
+- Updated dependencies [937ed39ce]
+- Updated dependencies [87c4f59de]
+- Updated dependencies [09b5fcf2e]
+- Updated dependencies [b42531cfe]
+- Updated dependencies [c2306f898]
+- Updated dependencies [9a9e7a42f]
+- Updated dependencies [50ce875a0]
+- Updated dependencies [ac6025f63]
+- Updated dependencies [e292e393f]
+- Updated dependencies [479b29124]
+ - @backstage/core@0.7.6
+ - @backstage/plugin-scaffolder-backend@0.10.0
+ - @backstage/cli@0.6.9
+ - @backstage/plugin-scaffolder@0.9.1
+ - @backstage/plugin-catalog-import@0.5.3
+ - @backstage/plugin-rollbar-backend@0.1.9
+ - @backstage/backend-common@0.6.3
+ - @backstage/plugin-catalog@0.5.5
+ - @backstage/plugin-catalog-backend@0.8.0
+ - @backstage/theme@0.2.6
+ - @backstage/plugin-techdocs@0.8.0
+
## 0.3.18
### Patch Changes
diff --git a/packages/create-app/package.json b/packages/create-app/package.json
index ea3d3548b8..071a0a5ebd 100644
--- a/packages/create-app/package.json
+++ b/packages/create-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
- "version": "0.3.18",
+ "version": "0.3.20",
"private": false,
"publishConfig": {
"access": "public"
diff --git a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js
index efcd5b8d93..43fb2e32de 100644
--- a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js
+++ b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js
@@ -1,6 +1,6 @@
describe('App', () => {
it('should render the catalog', () => {
cy.visit('/');
- cy.contains('My Company Service Catalog');
+ cy.contains('My Company Catalog');
});
});
diff --git a/packages/dev-utils/api-report.md b/packages/dev-utils/api-report.md
new file mode 100644
index 0000000000..d3e6447cd5
--- /dev/null
+++ b/packages/dev-utils/api-report.md
@@ -0,0 +1,26 @@
+## API Report File for "@backstage/dev-utils"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { ApiFactory } from '@backstage/core';
+import { ComponentType } from 'react';
+import { createPlugin } from '@backstage/core';
+import { Entity } from '@backstage/catalog-model';
+import { GridProps } from '@material-ui/core';
+import { IconComponent } from '@backstage/core';
+import { ReactNode } from 'react';
+
+// @public
+export function createDevApp(): DevAppBuilder;
+
+// @public (undocumented)
+export const EntityGridItem: ({ entity, classes, ...rest }: Omit, "container" | "item"> & {
+ entity: Entity;
+}) => JSX.Element;
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts
index c055bcd21e..ebf4e6c835 100644
--- a/packages/e2e-test/src/commands/run.ts
+++ b/packages/e2e-test/src/commands/run.ts
@@ -335,7 +335,7 @@ async function testAppServe(pluginName: string, appDir: string) {
try {
const browser = new Browser();
- await waitForPageWithText(browser, '/', 'My Company Service Catalog');
+ await waitForPageWithText(browser, '/', 'My Company Catalog');
await waitForPageWithText(
browser,
`/${pluginName}`,
diff --git a/packages/errors/api-report.md b/packages/errors/api-report.md
new file mode 100644
index 0000000000..cb24cba80f
--- /dev/null
+++ b/packages/errors/api-report.md
@@ -0,0 +1,88 @@
+## API Report File for "@backstage/errors"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { JsonObject } from '@backstage/config';
+
+// @public
+export class AuthenticationError extends CustomErrorBase {
+}
+
+// @public
+export class ConflictError extends CustomErrorBase {
+}
+
+// @public (undocumented)
+export class CustomErrorBase extends Error {
+ constructor(message?: string, cause?: Error);
+ // (undocumented)
+ readonly cause?: Error;
+}
+
+// @public
+export function deserializeError(data: SerializedError): T;
+
+// @public
+export type ErrorResponse = {
+ error: SerializedError;
+ request?: {
+ method: string;
+ url: string;
+ };
+ response: {
+ statusCode: number;
+ };
+};
+
+// @public
+export class InputError extends CustomErrorBase {
+}
+
+// @public
+export class NotAllowedError extends CustomErrorBase {
+}
+
+// @public
+export class NotFoundError extends CustomErrorBase {
+}
+
+// @public
+export class NotModifiedError extends CustomErrorBase {
+}
+
+// @public
+export function parseErrorResponse(response: Response): Promise;
+
+// @public
+export class ResponseError extends Error {
+ constructor(props: {
+ message: string;
+ response: Response;
+ data: ErrorResponse;
+ cause: Error;
+ });
+ readonly cause: Error;
+ readonly data: ErrorResponse;
+ static fromResponse(response: Response): Promise;
+ readonly response: Response;
+}
+
+// @public
+export type SerializedError = JsonObject & {
+ name: string;
+ message: string;
+ stack?: string;
+ code?: string;
+};
+
+// @public
+export function serializeError(error: Error, options?: {
+ includeStack?: boolean;
+}): SerializedError;
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/integration-react/api-report.md b/packages/integration-react/api-report.md
new file mode 100644
index 0000000000..7c37ab0392
--- /dev/null
+++ b/packages/integration-react/api-report.md
@@ -0,0 +1,23 @@
+## API Report File for "@backstage/integration-react"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { ApiRef } from '@backstage/core';
+import { Config } from '@backstage/config';
+import { ScmIntegrationRegistry } from '@backstage/integration';
+
+// @public (undocumented)
+export class ScmIntegrationsApi {
+ // (undocumented)
+ static fromConfig(config: Config): ScmIntegrationRegistry;
+}
+
+// @public (undocumented)
+export const scmIntegrationsApiRef: ApiRef;
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md
new file mode 100644
index 0000000000..5387eb12db
--- /dev/null
+++ b/packages/integration/api-report.md
@@ -0,0 +1,279 @@
+## API Report File for "@backstage/integration"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { Config } from '@backstage/config';
+
+// @public (undocumented)
+export class AzureIntegration implements ScmIntegration {
+ constructor(integrationConfig: AzureIntegrationConfig);
+ // (undocumented)
+ get config(): AzureIntegrationConfig;
+ // (undocumented)
+ static factory: ScmIntegrationsFactory;
+ // (undocumented)
+ resolveEditUrl(url: string): string;
+ // (undocumented)
+ resolveUrl(options: {
+ url: string;
+ base: string;
+ lineNumber?: number;
+ }): string;
+ // (undocumented)
+ get title(): string;
+ // (undocumented)
+ get type(): string;
+}
+
+// @public
+export type AzureIntegrationConfig = {
+ host: string;
+ token?: string;
+};
+
+// @public (undocumented)
+export class BitbucketIntegration implements ScmIntegration {
+ constructor(integrationConfig: BitbucketIntegrationConfig);
+ // (undocumented)
+ get config(): BitbucketIntegrationConfig;
+ // (undocumented)
+ static factory: ScmIntegrationsFactory;
+ // (undocumented)
+ resolveEditUrl(url: string): string;
+ // (undocumented)
+ resolveUrl(options: {
+ url: string;
+ base: string;
+ lineNumber?: number;
+ }): string;
+ // (undocumented)
+ get title(): string;
+ // (undocumented)
+ get type(): string;
+}
+
+// @public
+export type BitbucketIntegrationConfig = {
+ host: string;
+ apiBaseUrl?: string;
+ token?: string;
+ username?: string;
+ appPassword?: string;
+};
+
+// @public
+export function defaultScmResolveUrl(options: {
+ url: string;
+ base: string;
+ lineNumber?: number;
+}): string;
+
+// @public
+export function getAzureCommitsUrl(url: string): string;
+
+// @public
+export function getAzureDownloadUrl(url: string): string;
+
+// @public
+export function getAzureFileFetchUrl(url: string): string;
+
+// @public
+export function getAzureRequestOptions(config: AzureIntegrationConfig, additionalHeaders?: Record): RequestInit;
+
+// @public
+export function getBitbucketDefaultBranch(url: string, config: BitbucketIntegrationConfig): Promise;
+
+// @public
+export function getBitbucketDownloadUrl(url: string, config: BitbucketIntegrationConfig): Promise;
+
+// @public
+export function getBitbucketFileFetchUrl(url: string, config: BitbucketIntegrationConfig): string;
+
+// @public
+export function getBitbucketRequestOptions(config: BitbucketIntegrationConfig): RequestInit;
+
+// @public
+export function getGitHubFileFetchUrl(url: string, config: GitHubIntegrationConfig): string;
+
+// @public
+export function getGitHubRequestOptions(config: GitHubIntegrationConfig): RequestInit;
+
+// @public
+export function getGitLabFileFetchUrl(url: string, config: GitLabIntegrationConfig): Promise;
+
+// @public
+export function getGitLabRequestOptions(config: GitLabIntegrationConfig): RequestInit;
+
+// @public (undocumented)
+export class GithubCredentialsProvider {
+ // (undocumented)
+ static create(config: GitHubIntegrationConfig): GithubCredentialsProvider;
+ getCredentials(opts: {
+ url: string;
+ }): Promise;
+ }
+
+// @public (undocumented)
+export class GitHubIntegration implements ScmIntegration {
+ constructor(integrationConfig: GitHubIntegrationConfig);
+ // (undocumented)
+ get config(): GitHubIntegrationConfig;
+ // (undocumented)
+ static factory: ScmIntegrationsFactory;
+ // (undocumented)
+ resolveEditUrl(url: string): string;
+ // (undocumented)
+ resolveUrl(options: {
+ url: string;
+ base: string;
+ lineNumber?: number;
+ }): string;
+ // (undocumented)
+ get title(): string;
+ // (undocumented)
+ get type(): string;
+}
+
+// @public
+export type GitHubIntegrationConfig = {
+ host: string;
+ apiBaseUrl?: string;
+ rawBaseUrl?: string;
+ token?: string;
+ apps?: GithubAppConfig[];
+};
+
+// @public (undocumented)
+export class GitLabIntegration implements ScmIntegration {
+ constructor(integrationConfig: GitLabIntegrationConfig);
+ // (undocumented)
+ get config(): GitLabIntegrationConfig;
+ // (undocumented)
+ static factory: ScmIntegrationsFactory;
+ // (undocumented)
+ resolveEditUrl(url: string): string;
+ // (undocumented)
+ resolveUrl(options: {
+ url: string;
+ base: string;
+ lineNumber?: number;
+ }): string;
+ // (undocumented)
+ get title(): string;
+ // (undocumented)
+ get type(): string;
+}
+
+// @public
+export type GitLabIntegrationConfig = {
+ host: string;
+ apiBaseUrl: string;
+ token?: string;
+ baseUrl: string;
+};
+
+// @public
+export type GoogleGcsIntegrationConfig = {
+ clientEmail?: string;
+ privateKey?: string;
+};
+
+// @public
+export function readAzureIntegrationConfig(config: Config): AzureIntegrationConfig;
+
+// @public
+export function readAzureIntegrationConfigs(configs: Config[]): AzureIntegrationConfig[];
+
+// @public
+export function readBitbucketIntegrationConfig(config: Config): BitbucketIntegrationConfig;
+
+// @public
+export function readBitbucketIntegrationConfigs(configs: Config[]): BitbucketIntegrationConfig[];
+
+// @public
+export function readGitHubIntegrationConfig(config: Config): GitHubIntegrationConfig;
+
+// @public
+export function readGitHubIntegrationConfigs(configs: Config[]): GitHubIntegrationConfig[];
+
+// @public
+export function readGitLabIntegrationConfig(config: Config): GitLabIntegrationConfig;
+
+// @public
+export function readGitLabIntegrationConfigs(configs: Config[]): GitLabIntegrationConfig[];
+
+// @public
+export function readGoogleGcsIntegrationConfig(config: Config): GoogleGcsIntegrationConfig;
+
+// @public
+export interface ScmIntegration {
+ resolveEditUrl(url: string): string;
+ resolveUrl(options: {
+ url: string;
+ base: string;
+ lineNumber?: number;
+ }): string;
+ title: string;
+ type: string;
+}
+
+// @public
+export interface ScmIntegrationRegistry extends ScmIntegrationsGroup {
+ // (undocumented)
+ azure: ScmIntegrationsGroup;
+ // (undocumented)
+ bitbucket: ScmIntegrationsGroup;
+ // (undocumented)
+ github: ScmIntegrationsGroup;
+ // (undocumented)
+ gitlab: ScmIntegrationsGroup;
+ resolveEditUrl(url: string): string;
+ resolveUrl(options: {
+ url: string;
+ base: string;
+ lineNumber?: number;
+ }): string;
+}
+
+// @public (undocumented)
+export class ScmIntegrations implements ScmIntegrationRegistry {
+ constructor(integrationsByType: IntegrationsByType);
+ // (undocumented)
+ get azure(): ScmIntegrationsGroup;
+ // (undocumented)
+ get bitbucket(): ScmIntegrationsGroup;
+ // (undocumented)
+ byHost(host: string): ScmIntegration | undefined;
+ // (undocumented)
+ byUrl(url: string | URL): ScmIntegration | undefined;
+ // (undocumented)
+ static fromConfig(config: Config): ScmIntegrations;
+ // (undocumented)
+ get github(): ScmIntegrationsGroup;
+ // (undocumented)
+ get gitlab(): ScmIntegrationsGroup;
+ // (undocumented)
+ list(): ScmIntegration[];
+ // (undocumented)
+ resolveEditUrl(url: string): string;
+ // (undocumented)
+ resolveUrl(options: {
+ url: string;
+ base: string;
+ lineNumber?: number;
+ }): string;
+}
+
+// @public
+export interface ScmIntegrationsGroup {
+ byHost(host: string): T | undefined;
+ byUrl(url: string | URL): T | undefined;
+ list(): T[];
+}
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/search-common/api-report.md b/packages/search-common/api-report.md
new file mode 100644
index 0000000000..76bd937a1c
--- /dev/null
+++ b/packages/search-common/api-report.md
@@ -0,0 +1,55 @@
+## API Report File for "@backstage/search-common"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { JsonObject } from '@backstage/config';
+
+// @public
+export interface DocumentCollator {
+ // (undocumented)
+ execute(): Promise;
+}
+
+// @public
+export interface DocumentDecorator {
+ // (undocumented)
+ execute(documents: IndexableDocument[]): Promise;
+}
+
+// @public
+export interface IndexableDocument {
+ location: string;
+ text: string;
+ title: string;
+}
+
+// @public (undocumented)
+export interface SearchQuery {
+ // (undocumented)
+ filters?: JsonObject;
+ // (undocumented)
+ pageCursor: string;
+ // (undocumented)
+ term: string;
+ // (undocumented)
+ types?: string[];
+}
+
+// @public (undocumented)
+export interface SearchResult {
+ // (undocumented)
+ document: IndexableDocument;
+}
+
+// @public (undocumented)
+export interface SearchResultSet {
+ // (undocumented)
+ results: SearchResult[];
+}
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts
index ad784c3b36..daa5823424 100644
--- a/packages/search-common/src/types.ts
+++ b/packages/search-common/src/types.ts
@@ -18,6 +18,7 @@ import { JsonObject } from '@backstage/config';
export interface SearchQuery {
term: string;
filters?: JsonObject;
+ types?: string[];
pageCursor: string;
}
diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md
index 9dd6447a14..1788502e45 100644
--- a/packages/techdocs-common/CHANGELOG.md
+++ b/packages/techdocs-common/CHANGELOG.md
@@ -1,5 +1,11 @@
# @backstage/techdocs-common
+## 0.5.1
+
+### Patch Changes
+
+- f4af06ebe: Gracefully handle HTTP request failures in download method of AzureBlobStorage publisher.
+
## 0.5.0
### Minor Changes
diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md
new file mode 100644
index 0000000000..a53fb9e681
--- /dev/null
+++ b/packages/techdocs-common/api-report.md
@@ -0,0 +1,178 @@
+## API Report File for "@backstage/techdocs-common"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { AzureIntegrationConfig } from '@backstage/integration';
+import { Config } from '@backstage/config';
+import Docker from 'dockerode';
+import { Entity } from '@backstage/catalog-model';
+import { EntityName } from '@backstage/catalog-model';
+import express from 'express';
+import { GitHubIntegrationConfig } from '@backstage/integration';
+import { GitLabIntegrationConfig } from '@backstage/integration';
+import { Logger } from 'winston';
+import { PluginEndpointDiscovery } from '@backstage/backend-common';
+import { UrlReader } from '@backstage/backend-common';
+import { Writable } from 'stream';
+
+// @public (undocumented)
+export const checkoutGitRepository: (repoUrl: string, config: Config, logger: Logger) => Promise;
+
+// @public (undocumented)
+export class CommonGitPreparer implements PreparerBase {
+ constructor(config: Config, logger: Logger);
+ // (undocumented)
+ prepare(entity: Entity, options?: {
+ etag?: string;
+ }): Promise;
+}
+
+// @public (undocumented)
+export class DirectoryPreparer implements PreparerBase {
+ constructor(config: Config, logger: Logger, reader: UrlReader);
+ // (undocumented)
+ prepare(entity: Entity): Promise;
+ }
+
+// @public (undocumented)
+export type GeneratorBase = {
+ run(opts: GeneratorRunOptions): Promise;
+};
+
+// @public
+export type GeneratorBuilder = {
+ register(protocol: SupportedGeneratorKey, generator: GeneratorBase): void;
+ get(entity: Entity): GeneratorBase;
+};
+
+// @public (undocumented)
+export class Generators implements GeneratorBuilder {
+ // (undocumented)
+ static fromConfig(config: Config, { logger }: {
+ logger: Logger;
+ }): Promise;
+ // (undocumented)
+ get(entity: Entity): GeneratorBase;
+ // (undocumented)
+ register(generatorKey: SupportedGeneratorKey, generator: GeneratorBase): void;
+}
+
+// @public (undocumented)
+export const getAzureIntegrationConfig: (config: Config, host: string) => AzureIntegrationConfig;
+
+// @public (undocumented)
+export const getDefaultBranch: (repositoryUrl: string, config: Config) => Promise;
+
+// @public (undocumented)
+export const getDocFilesFromRepository: (reader: UrlReader, entity: Entity, opts?: {
+ etag?: string | undefined;
+ logger?: Logger | undefined;
+} | undefined) => Promise;
+
+// @public (undocumented)
+export function getGitHost(url: string): string;
+
+// @public (undocumented)
+export const getGitHubIntegrationConfig: (config: Config, host: string) => GitHubIntegrationConfig;
+
+// @public (undocumented)
+export const getGitLabIntegrationConfig: (config: Config, host: string) => GitLabIntegrationConfig;
+
+// @public (undocumented)
+export const getGitRepositoryTempFolder: (repositoryUrl: string, config: Config) => Promise;
+
+// @public (undocumented)
+export function getGitRepoType(url: string): string;
+
+// @public (undocumented)
+export const getLastCommitTimestamp: (repositoryLocation: string, logger: Logger) => Promise;
+
+// @public (undocumented)
+export const getLocationForEntity: (entity: Entity) => ParsedLocationAnnotation;
+
+// @public (undocumented)
+export const getTokenForGitRepo: (repositoryUrl: string, config: Config) => Promise;
+
+// @public (undocumented)
+export type ParsedLocationAnnotation = {
+ type: RemoteProtocol;
+ target: string;
+};
+
+// @public (undocumented)
+export const parseReferenceAnnotation: (annotationName: string, entity: Entity) => ParsedLocationAnnotation;
+
+// @public (undocumented)
+export type PreparerBase = {
+ prepare(entity: Entity, options?: {
+ logger?: Logger;
+ etag?: string;
+ }): Promise;
+};
+
+// @public (undocumented)
+export type PreparerBuilder = {
+ register(protocol: RemoteProtocol, preparer: PreparerBase): void;
+ get(entity: Entity): PreparerBase;
+};
+
+// @public (undocumented)
+export class Preparers implements PreparerBuilder {
+ // (undocumented)
+ static fromConfig(config: Config, { logger, reader }: factoryOptions): Promise;
+ // (undocumented)
+ get(entity: Entity): PreparerBase;
+ // (undocumented)
+ register(protocol: RemoteProtocol, preparer: PreparerBase): void;
+}
+
+// @public
+export class Publisher {
+ // (undocumented)
+ static fromConfig(config: Config, { logger, discovery }: factoryOptions_2): Promise;
+}
+
+// @public
+export interface PublisherBase {
+ docsRouter(): express.Handler;
+ fetchTechDocsMetadata(entityName: EntityName): Promise;
+ getReadiness(): Promise;
+ hasDocsBeenGenerated(entityName: Entity): Promise;
+ publish(request: PublishRequest): Promise;
+}
+
+// @public
+export type PublisherType = 'local' | 'googleGcs' | 'awsS3' | 'azureBlobStorage' | 'openStackSwift';
+
+// @public
+export type RemoteProtocol = 'url' | 'dir' | 'github' | 'gitlab' | 'file' | 'azure/api';
+
+// @public (undocumented)
+export class TechdocsGenerator implements GeneratorBase {
+ constructor(logger: Logger, config: Config);
+ // (undocumented)
+ run({ inputDir, outputDir, dockerClient, parsedLocationAnnotation, etag, }: GeneratorRunOptions): Promise;
+}
+
+// @public
+export type TechDocsMetadata = {
+ site_name: string;
+ site_description: string;
+ etag: string;
+};
+
+// @public (undocumented)
+export class UrlPreparer implements PreparerBase {
+ constructor(reader: UrlReader, logger: Logger);
+ // (undocumented)
+ prepare(entity: Entity, options?: {
+ etag?: string;
+ }): Promise;
+ }
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json
index c41dce72b1..e749b33919 100644
--- a/packages/techdocs-common/package.json
+++ b/packages/techdocs-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/techdocs-common",
"description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli",
- "version": "0.5.0",
+ "version": "0.5.1",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts
index 15da3b7cfc..f094457346 100644
--- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts
+++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts
@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { getVoidLogger } from '@backstage/backend-common';
import {
Entity,
- ENTITY_DEFAULT_NAMESPACE,
EntityName,
+ ENTITY_DEFAULT_NAMESPACE,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import mockFs from 'mock-fs';
@@ -154,7 +155,6 @@ describe('AwsS3Publish', () => {
});
it('should fail to publish a directory', async () => {
- expect.assertions(3);
const wrongPathToGeneratedDirectory = path.join(
rootDir,
'wrong',
@@ -171,23 +171,22 @@ describe('AwsS3Publish', () => {
}),
).rejects.toThrowError();
- await publisher
- .publish({
- entity,
- directory: wrongPathToGeneratedDirectory,
- })
- .catch(error => {
- expect(error.message).toEqual(
- // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
- // Issue reported https://github.com/tschaub/mock-fs/issues/118
- expect.stringContaining(
- `Unable to upload file(s) to AWS S3. Error: Failed to read template directory: ENOENT, no such file or directory`,
- ),
- );
- expect(error.message).toEqual(
- expect.stringContaining(wrongPathToGeneratedDirectory),
- );
- });
+ const fails = publisher.publish({
+ entity,
+ directory: wrongPathToGeneratedDirectory,
+ });
+
+ // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
+ // Issue reported https://github.com/tschaub/mock-fs/issues/118
+ await expect(fails).rejects.toMatchObject({
+ message: expect.stringContaining(
+ 'Unable to upload file(s) to AWS S3. Error: Failed to read template directory: ENOENT, no such file or directory',
+ ),
+ });
+ await expect(fails).rejects.toMatchObject({
+ message: expect.stringContaining(wrongPathToGeneratedDirectory),
+ });
+
mockFs.restore();
});
});
@@ -265,18 +264,12 @@ describe('AwsS3Publish', () => {
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
- await publisher
- .fetchTechDocsMetadata(entityNameMock)
- .catch(error =>
- expect(error).toEqual(
- new Error(
- `TechDocs metadata fetch failed, The file ${path.join(
- entityRootDir,
- 'techdocs_metadata.json',
- )} does not exist !`,
- ),
- ),
- );
+ const fails = publisher.fetchTechDocsMetadata(entityNameMock);
+
+ const errorPath = path.join(entityRootDir, 'techdocs_metadata.json');
+ await expect(fails).rejects.toMatchObject({
+ message: `TechDocs metadata fetch failed, The file ${errorPath} does not exist !`,
+ });
});
});
});
diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts
index 9c6684fdca..d628c860fd 100644
--- a/packages/techdocs-common/src/stages/publish/awsS3.ts
+++ b/packages/techdocs-common/src/stages/publish/awsS3.ts
@@ -80,10 +80,17 @@ export class AwsS3Publish implements PublisherBase {
'techdocs.publisher.awsS3.endpoint',
);
+ // AWS forcePathStyle is an optional config. If missing, it defaults to false. Needs to be enabled for cases
+ // where endpoint url points to locally hosted S3 compatible storage like Localstack
+ const s3ForcePathStyle = config.getOptionalBoolean(
+ 'techdocs.publisher.awsS3.s3ForcePathStyle',
+ );
+
const storageClient = new aws.S3({
credentials,
...(region && { region }),
...(endpoint && { endpoint }),
+ ...(s3ForcePathStyle && { s3ForcePathStyle }),
});
return new AwsS3Publish(storageClient, bucketName, logger);
diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts
index c431e924a8..1160ffe49d 100644
--- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts
+++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts
@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { getVoidLogger } from '@backstage/backend-common';
import {
Entity,
- ENTITY_DEFAULT_NAMESPACE,
EntityName,
+ ENTITY_DEFAULT_NAMESPACE,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import mockFs from 'mock-fs';
@@ -157,7 +158,6 @@ describe('publishing with valid credentials', () => {
});
it('should fail to publish a directory', async () => {
- expect.assertions(1);
const wrongPathToGeneratedDirectory = path.join(
rootDir,
'wrong',
@@ -168,22 +168,20 @@ describe('publishing with valid credentials', () => {
const entity = createMockEntity();
- await publisher
- .publish({
- entity,
- directory: wrongPathToGeneratedDirectory,
- })
- .catch(error => {
- // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
- // Issue reported https://github.com/tschaub/mock-fs/issues/118
- expect.stringContaining(
- `Unable to upload file(s) to Azure Blob Storage. Error: Failed to read template directory: ENOENT, no such file or directory`,
- );
+ const fails = publisher.publish({
+ entity,
+ directory: wrongPathToGeneratedDirectory,
+ });
+
+ await expect(fails).rejects.toMatchObject({
+ message: expect.stringContaining(
+ `Unable to upload file(s) to Azure Blob Storage. Error: Failed to read template directory: ENOENT, no such file or directory`,
+ ),
+ });
+ await expect(fails).rejects.toMatchObject({
+ message: expect.stringContaining(wrongPathToGeneratedDirectory),
+ });
- expect(error.message).toEqual(
- expect.stringContaining(wrongPathToGeneratedDirectory),
- );
- });
mockFs.restore();
});
diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts
index 1a823c3d19..f6567abe49 100644
--- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts
+++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts
@@ -221,7 +221,8 @@ export class AzureBlobStoragePublish implements PublisherBase {
.on('end', () => {
resolve(Buffer.concat(fileStreamChunks));
});
- });
+ })
+ .catch(reject);
});
}
diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts
index 7e580b16bc..19e4938763 100644
--- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts
+++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { getVoidLogger } from '@backstage/backend-common';
import {
Entity,
@@ -146,8 +147,6 @@ describe('GoogleGCSPublish', () => {
});
it('should fail to publish a directory', async () => {
- expect.assertions(3);
-
const wrongPathToGeneratedDirectory = path.join(
rootDir,
'wrong',
@@ -165,23 +164,21 @@ describe('GoogleGCSPublish', () => {
}),
).rejects.toThrowError();
- await publisher
- .publish({
- entity,
- directory: wrongPathToGeneratedDirectory,
- })
- .catch(error => {
- expect(error.message).toEqual(
- // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
- // Issue reported https://github.com/tschaub/mock-fs/issues/118
- expect.stringContaining(
- `Unable to upload file(s) to Google Cloud Storage. Error: Failed to read template directory: ENOENT, no such file or directory`,
- ),
- );
- expect(error.message).toEqual(
- expect.stringContaining(wrongPathToGeneratedDirectory),
- );
- });
+ const fails = publisher.publish({
+ entity,
+ directory: wrongPathToGeneratedDirectory,
+ });
+
+ // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
+ // Issue reported https://github.com/tschaub/mock-fs/issues/118
+ await expect(fails).rejects.toMatchObject({
+ message: expect.stringContaining(
+ `Unable to upload file(s) to Google Cloud Storage. Error: Failed to read template directory: ENOENT, no such file or directory`,
+ ),
+ });
+ await expect(fails).rejects.toMatchObject({
+ message: expect.stringContaining(wrongPathToGeneratedDirectory),
+ });
mockFs.restore();
});
@@ -264,16 +261,14 @@ describe('GoogleGCSPublish', () => {
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
- await publisher
- .fetchTechDocsMetadata(entityNameMock)
- .catch(errorMessage =>
- expect(errorMessage).toEqual(
- `The file ${path.join(
- entityRootDir,
- 'techdocs_metadata.json',
- )} does not exist !`,
- ),
- );
+ const fails = publisher.fetchTechDocsMetadata(entityNameMock);
+
+ await expect(fails).rejects.toMatchObject({
+ message: `The file ${path.join(
+ entityRootDir,
+ 'techdocs_metadata.json',
+ )} does not exist !`,
+ });
});
});
});
diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts
index 28fbb42ece..8adafc361a 100644
--- a/packages/techdocs-common/src/stages/publish/googleStorage.ts
+++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts
@@ -168,7 +168,7 @@ export class GoogleGCSPublish implements PublisherBase {
.createReadStream()
.on('error', err => {
this.logger.error(err.message);
- reject(err.message);
+ reject(err);
})
.on('data', chunk => {
fileStreamChunks.push(chunk);
diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts
index 6f0aacede0..e179aeb9a7 100644
--- a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts
+++ b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts
@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { getVoidLogger } from '@backstage/backend-common';
import {
Entity,
- ENTITY_DEFAULT_NAMESPACE,
EntityName,
+ ENTITY_DEFAULT_NAMESPACE,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import mockFs from 'mock-fs';
@@ -157,7 +158,6 @@ describe('OpenStackSwiftPublish', () => {
});
it('should fail to publish a directory', async () => {
- expect.assertions(3);
const wrongPathToGeneratedDirectory = path.join(
rootDir,
'wrong',
@@ -174,23 +174,22 @@ describe('OpenStackSwiftPublish', () => {
}),
).rejects.toThrowError();
- await publisher
- .publish({
- entity,
- directory: wrongPathToGeneratedDirectory,
- })
- .catch(error => {
- expect(error.message).toEqual(
- // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
- // Issue reported https://github.com/tschaub/mock-fs/issues/118
- expect.stringContaining(
- `Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT, no such file or directory`,
- ),
- );
- expect(error.message).toEqual(
- expect.stringContaining(wrongPathToGeneratedDirectory),
- );
- });
+ const fails = publisher.publish({
+ entity,
+ directory: wrongPathToGeneratedDirectory,
+ });
+
+ // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
+ // Issue reported https://github.com/tschaub/mock-fs/issues/118
+ await expect(fails).rejects.toMatchObject({
+ message: expect.stringContaining(
+ `Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT, no such file or directory`,
+ ),
+ });
+ await expect(fails).rejects.toMatchObject({
+ message: expect.stringContaining(wrongPathToGeneratedDirectory),
+ });
+
mockFs.restore();
});
});
@@ -268,18 +267,14 @@ describe('OpenStackSwiftPublish', () => {
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
- await publisher
- .fetchTechDocsMetadata(entityNameMock)
- .catch(error =>
- expect(error).toEqual(
- new Error(
- `TechDocs metadata fetch failed, The file ${path.join(
- entityRootDir,
- 'techdocs_metadata.json',
- )} does not exist !`,
- ),
- ),
- );
+ const fails = publisher.fetchTechDocsMetadata(entityNameMock);
+
+ await expect(fails).rejects.toMatchObject({
+ message: `TechDocs metadata fetch failed, The file ${path.join(
+ entityRootDir,
+ 'techdocs_metadata.json',
+ )} does not exist !`,
+ });
});
});
});
diff --git a/packages/test-utils-core/api-report.md b/packages/test-utils-core/api-report.md
new file mode 100644
index 0000000000..4b603b8e7b
--- /dev/null
+++ b/packages/test-utils-core/api-report.md
@@ -0,0 +1,86 @@
+## API Report File for "@backstage/test-utils-core"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { ReactElement } from 'react';
+import { RenderResult } from '@testing-library/react';
+
+// @public (undocumented)
+export type AsyncLogCollector = () => Promise;
+
+// @public (undocumented)
+export type CollectedLogs = {
+ [key in T]: string[];
+};
+
+// @public (undocumented)
+export class Keyboard {
+ constructor(target: any, { debug }?: {
+ debug?: boolean | undefined;
+ });
+ // (undocumented)
+ click(): Promise;
+ // (undocumented)
+ debug: boolean;
+ // (undocumented)
+ document: any;
+ // (undocumented)
+ enter(value: any): Promise;
+ // (undocumented)
+ escape(): Promise;
+ // (undocumented)
+ get focused(): any;
+ // (undocumented)
+ static fromReadableInput(input: any): any;
+ // (undocumented)
+ _log(message: any, ...args: any[]): void;
+ // (undocumented)
+ _pretty(element: any): string;
+ // (undocumented)
+ send(chars: any): Promise;
+ // (undocumented)
+ _sendKey(key: any, charCode: any, action: any): Promise;
+ // (undocumented)
+ tab(): Promise;
+ // (undocumented)
+ static toReadableInput(chars: any): any;
+ // (undocumented)
+ toString(): string;
+ // (undocumented)
+ static type(target: any, input: any): Promise;
+ // (undocumented)
+ type(input: any): Promise;
+ // (undocumented)
+ static typeDebug(target: any, input: any): Promise;
+}
+
+// @public (undocumented)
+export type LogCollector = AsyncLogCollector | SyncLogCollector;
+
+// @public (undocumented)
+export type LogFuncs = 'log' | 'warn' | 'error';
+
+// @public (undocumented)
+export function renderWithEffects(nodes: ReactElement): Promise;
+
+// @public (undocumented)
+export type SyncLogCollector = () => void;
+
+// @public (undocumented)
+export function withLogCollector(callback: AsyncLogCollector): Promise>;
+
+// @public (undocumented)
+export function withLogCollector(callback: SyncLogCollector): CollectedLogs;
+
+// @public (undocumented)
+export function withLogCollector(logsToCollect: T[], callback: AsyncLogCollector): Promise>;
+
+// @public (undocumented)
+export function withLogCollector(logsToCollect: T[], callback: SyncLogCollector): CollectedLogs;
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md
new file mode 100644
index 0000000000..dcac9b64a7
--- /dev/null
+++ b/packages/test-utils/api-report.md
@@ -0,0 +1,82 @@
+## API Report File for "@backstage/test-utils"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { ComponentType } from 'react';
+import { ErrorApi } from '@backstage/core-api';
+import { ErrorContext } from '@backstage/core-api';
+import { ExternalRouteRef } from '@backstage/core-api';
+import { Observable } from '@backstage/core-api';
+import { ReactElement } from 'react';
+import { ReactNode } from 'react';
+import { RenderResult } from '@testing-library/react';
+import { RouteRef } from '@backstage/core-api';
+import { StorageApi } from '@backstage/core-api';
+import { StorageValueChange } from '@backstage/core-api';
+
+// @public (undocumented)
+export function mockBreakpoint(initialBreakpoint?: Breakpoint): {
+ set(breakpoint: Breakpoint): void;
+ remove(): void;
+};
+
+// @public (undocumented)
+export class MockErrorApi implements ErrorApi {
+ constructor(options?: Options);
+ // (undocumented)
+ error$(): Observable<{
+ error: Error;
+ context?: ErrorContext;
+ }>;
+ // (undocumented)
+ getErrors(): ErrorWithContext[];
+ // (undocumented)
+ post(error: Error, context?: ErrorContext): void;
+ // (undocumented)
+ waitForError(pattern: RegExp, timeoutMs?: number): Promise;
+}
+
+// @public (undocumented)
+export class MockStorageApi implements StorageApi {
+ // (undocumented)
+ static create(data?: MockStorageBucket): MockStorageApi;
+ // (undocumented)
+ forBucket(name: string): StorageApi;
+ // (undocumented)
+ get(key: string): T | undefined;
+ // (undocumented)
+ observe$(key: string): Observable>;
+ // (undocumented)
+ remove(key: string): Promise;
+ // (undocumented)
+ set(key: string, data: T): Promise;
+ }
+
+// @public (undocumented)
+export type MockStorageBucket = {
+ [key: string]: any;
+};
+
+// @public (undocumented)
+export const msw: {
+ setupDefaultHandlers: (worker: {
+ listen: (t: any) => void;
+ close: () => void;
+ resetHandlers: () => void;
+ }) => void;
+};
+
+// @public
+export function renderInTestApp(Component: ComponentType | ReactNode, options?: TestAppOptions): Promise;
+
+// @public
+export function wrapInTestApp(Component: ComponentType | ReactNode, options?: TestAppOptions): ReactElement;
+
+
+export * from "@backstage/test-utils-core";
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md
index 9ce3c75802..22964d6b25 100644
--- a/packages/theme/CHANGELOG.md
+++ b/packages/theme/CHANGELOG.md
@@ -1,5 +1,11 @@
# @backstage/theme
+## 0.2.6
+
+### Patch Changes
+
+- 931b21a12: Replace the link color in dark theme
+
## 0.2.5
### Patch Changes
diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md
new file mode 100644
index 0000000000..93d0957f5a
--- /dev/null
+++ b/packages/theme/api-report.md
@@ -0,0 +1,89 @@
+## API Report File for "@backstage/theme"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { Overrides } from '@material-ui/core/styles/overrides';
+import { Palette } from '@material-ui/core/styles/createPalette';
+import { PaletteOptions } from '@material-ui/core/styles/createPalette';
+import { Theme } from '@material-ui/core';
+import { ThemeOptions } from '@material-ui/core';
+
+// @public (undocumented)
+export type BackstagePalette = Palette & PaletteAdditions;
+
+// @public (undocumented)
+export type BackstagePaletteOptions = PaletteOptions & PaletteAdditions;
+
+// @public (undocumented)
+export interface BackstageTheme extends Theme {
+ // (undocumented)
+ getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme;
+ // (undocumented)
+ page: PageTheme;
+ // (undocumented)
+ palette: BackstagePalette;
+}
+
+// @public (undocumented)
+export interface BackstageThemeOptions extends ThemeOptions {
+ // (undocumented)
+ getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme;
+ // (undocumented)
+ page: PageTheme;
+ // (undocumented)
+ palette: BackstagePaletteOptions;
+}
+
+// @public (undocumented)
+export const colorVariants: Record;
+
+// @public (undocumented)
+export function createTheme(options: SimpleThemeOptions): BackstageTheme;
+
+// @public (undocumented)
+export function createThemeOptions(options: SimpleThemeOptions): BackstageThemeOptions;
+
+// @public (undocumented)
+export function createThemeOverrides(theme: BackstageTheme): Overrides;
+
+// @public (undocumented)
+export const darkTheme: BackstageTheme;
+
+// @public (undocumented)
+export function genPageTheme(colors: string[], shape: string): PageTheme;
+
+// @public (undocumented)
+export const lightTheme: BackstageTheme;
+
+// @public (undocumented)
+export type PageTheme = {
+ colors: string[];
+ shape: string;
+ backgroundImage: string;
+};
+
+// @public (undocumented)
+export const pageTheme: Record;
+
+// @public (undocumented)
+export type PageThemeSelector = {
+ themeId: string;
+};
+
+// @public (undocumented)
+export const shapes: Record;
+
+// @public
+export type SimpleThemeOptions = {
+ palette: BackstagePaletteOptions;
+ defaultPageTheme: string;
+ pageTheme?: Record;
+ fontFamily?: string;
+};
+
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/theme/package.json b/packages/theme/package.json
index b10e01ca1b..87821b6751 100644
--- a/packages/theme/package.json
+++ b/packages/theme/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
- "version": "0.2.5",
+ "version": "0.2.6",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,7 +31,7 @@
"@material-ui/core": "^4.11.0"
},
"devDependencies": {
- "@backstage/cli": "^0.6.6"
+ "@backstage/cli": "^0.6.9"
},
"files": [
"dist"
diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts
index 0cdd41508c..94cf38656e 100644
--- a/packages/theme/src/themes.ts
+++ b/packages/theme/src/themes.ts
@@ -102,7 +102,8 @@ export const darkTheme = createTheme({
},
},
primary: {
- main: '#8CB4E5',
+ main: '#9CC9FF',
+ dark: '#82BAFD',
},
secondary: {
main: '#FF88B2',
@@ -124,8 +125,8 @@ export const darkTheme = createTheme({
errorText: '#CA001B',
infoText: '#004e8a',
warningText: '#000000',
- linkHover: '#2196F3',
- link: '#0A6EBE',
+ linkHover: '#82BAFD',
+ link: '#9CC9FF',
gold: yellow.A700,
navigation: {
background: '#424242',
diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md
index a2d5a28e41..f268ba1c4d 100644
--- a/plugins/api-docs/CHANGELOG.md
+++ b/plugins/api-docs/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-api-docs
+## 0.4.12
+
+### Patch Changes
+
+- 1ce80ff02: Resolve issues with AsyncAPI rendering by updating `@asyncapi/react-component`
+ to `0.23.0`. The theming of the component is adjusted to the latest styling
+ changes.
+- c614ede9a: Updated README to have up-to-date install instructions.
+- 07a7806c3: Added fields filtering in get API entities to avoid the requesting of unused data
+- Updated dependencies [9afcac5af]
+- Updated dependencies [e0c9ed759]
+- Updated dependencies [6eaecbd81]
+ - @backstage/core@0.7.7
+
## 0.4.11
### Patch Changes
diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md
index 4e71e87275..dac4b6ff4d 100644
--- a/plugins/api-docs/README.md
+++ b/plugins/api-docs/README.md
@@ -33,17 +33,7 @@ To link that a component provides or consumes an API, see the [`providesApis`](h
yarn add @backstage/plugin-api-docs
```
-2. Add the plugin to the app:
-
-```ts
-// packages/app/src/plugins.ts
-
-export { apiDocsPlugin } from '@backstage/plugin-api-docs';
-```
-
-} />
-
-3. Register the `ApiExplorerPage` at the `/api-docs` path:
+2. Add the `ApiExplorerPage` extension to the app:
```tsx
// packages/app/src/App.tsx
@@ -53,7 +43,7 @@ import { ApiExplorerPage } from '@backstage/plugin-api-docs';
} />;
```
-4. Add one of the provided widgets to the EntityPage:
+3. Add one of the provided widgets to the EntityPage:
```tsx
// packages/app/src/components/catalog/EntityPage.tsx
diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json
index 13f27c321b..5c9f9ca7ee 100644
--- a/plugins/api-docs/package.json
+++ b/plugins/api-docs/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
- "version": "0.4.11",
+ "version": "0.4.12",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,11 +29,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@asyncapi/react-component": "^0.22.3",
+ "@asyncapi/react-component": "^0.23.0",
"@backstage/catalog-model": "^0.7.7",
- "@backstage/core": "^0.7.5",
+ "@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.4",
- "@backstage/theme": "^0.2.5",
+ "@backstage/theme": "^0.2.6",
"@material-icons/font": "^1.0.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -49,7 +49,7 @@
"swagger-ui-react": "^3.37.2"
},
"devDependencies": {
- "@backstage/cli": "^0.6.8",
+ "@backstage/cli": "^0.6.9",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx
index 83d0bfc2ba..1ed47811ef 100644
--- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx
+++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx
@@ -34,7 +34,19 @@ export const ApiExplorerPage = () => {
const createComponentLink = useRouteRef(createComponentRouteRef);
const catalogApi = useApi(catalogApiRef);
const { loading, error, value: catalogResponse } = useAsync(() => {
- return catalogApi.getEntities({ filter: { kind: 'API' } });
+ return catalogApi.getEntities({
+ filter: { kind: 'API' },
+ fields: [
+ 'apiVersion',
+ 'kind',
+ 'metadata',
+ 'relations',
+ 'spec.lifecycle',
+ 'spec.owner',
+ 'spec.type',
+ 'spec.system',
+ ],
+ });
}, [catalogApi]);
return (
diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx
index a908a91b14..300c9d0c8a 100644
--- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx
+++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx
@@ -88,18 +88,10 @@ const useStyles = makeStyles(theme => ({
'& .asyncapi__enum': {
color: theme.palette.secondary.main,
},
- '& .asyncapi__toggle-arrow:before': {
- content: '">"',
- 'font-family': 'inherit',
- },
- '& .asyncapi__anchor-icon:before': {
- content: '"🔗"',
- 'font-family': 'inherit',
- },
'& .asyncapi__info, .asyncapi__channel, .asyncapi__channels > div, .asyncapi__schema, .asyncapi__channel-operations-list .asyncapi__messages-list-item .asyncapi__message, .asyncapi__message, .asyncapi__server, .asyncapi__servers > div, .asyncapi__messages > div, .asyncapi__schemas > div': {
'background-color': 'inherit',
},
- '& .asyncapi__channel-parameters-header, .asyncapi__channel-operations-header, .asyncapi__channel-operation-oneOf-subscribe-header, .asyncapi__channel-operation-oneOf-publish-header': {
+ '& .asyncapi__channel-parameters-header, .asyncapi__channel-operations-header, .asyncapi__channel-operation-oneOf-subscribe-header, .asyncapi__channel-operation-oneOf-publish-header, .asyncapi__channel-operation-message-header, .asyncapi__message-header, .asyncapi__message-header-title, .asyncapi__message-header-title > h3, .asyncapi__bindings, .asyncapi__bindings-header, .asyncapi__bindings-header > h4': {
'background-color': 'inherit',
color: theme.palette.text.primary,
},
diff --git a/plugins/badges/package.json b/plugins/badges/package.json
index 0d59e271b0..8f7b5865a1 100644
--- a/plugins/badges/package.json
+++ b/plugins/badges/package.json
@@ -21,10 +21,10 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.6",
- "@backstage/core": "^0.7.5",
+ "@backstage/core": "^0.7.7",
"@backstage/errors": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.3",
- "@backstage/theme": "^0.2.5",
+ "@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -34,7 +34,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.6.8",
+ "@backstage/cli": "^0.6.9",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md
index 66314933bb..ecb26efd50 100644
--- a/plugins/bitrise/CHANGELOG.md
+++ b/plugins/bitrise/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-bitrise
+## 0.1.2
+
+### Patch Changes
+
+- c614ede9a: Updated README to have up-to-date install instructions.
+- Updated dependencies [9afcac5af]
+- Updated dependencies [e0c9ed759]
+- Updated dependencies [6eaecbd81]
+ - @backstage/core@0.7.7
+
## 0.1.1
### Patch Changes
diff --git a/plugins/bitrise/README.md b/plugins/bitrise/README.md
index 59d85b0a98..411db3198e 100644
--- a/plugins/bitrise/README.md
+++ b/plugins/bitrise/README.md
@@ -8,36 +8,30 @@ Welcome to the Bitrise plugin!
## Installation
```sh
+# The plugin must be added in the app package
+$ cd packages/app
$ yarn add @backstage/plugin-bitrise
```
-Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) to enable the plugin:
-
-```js
-export { bitrisePlugin } from '@backstage/plugin-bitrise';
-```
-
-Bitrise Plugin exposes an "Entity Tab Content" component `EntityBitriseContent`. You can include it in the [`EntityPage.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/catalog/EntityPage.tsx)`:
+Bitrise Plugin exposes an entity tab component named `EntityBitriseContent`. You can include it in the
+[`EntityPage.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/catalog/EntityPage.tsx)`:
```tsx
// At the top imports
import { EntityBitriseContent } from '@backstage/plugin-bitrise';
-// Inside `WebsiteEntityPage` component
-
-
-
-
-
- }
-/>;
+// Farther down at the website declaration
+const websiteEntityPage = (
+
+ {/* Place the following section where you want the tab to appear */}
+
+
+
```
-Now your plugin should be visible in the entity page, however in a state with a missing `bitrise.io/app` annotation.
+Now your plugin should be visible as a tab at the top of the entity pages,
+specifically for components that are of the type `website`.
+However, it warns of a missing `bitrise.io/app` annotation.
Add the annotation to your component [catalog-info.yaml](https://github.com/backstage/backstage/blob/master/catalog-info.yaml) as shown in the highlighted example below:
diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json
index 16a02ae42a..950392f732 100644
--- a/plugins/bitrise/package.json
+++ b/plugins/bitrise/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-bitrise",
- "version": "0.1.1",
+ "version": "0.1.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,9 +21,9 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.2",
- "@backstage/core": "^0.7.5",
+ "@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.2",
- "@backstage/theme": "^0.2.5",
+ "@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -37,7 +37,7 @@
"recharts": "^1.8.5"
},
"devDependencies": {
- "@backstage/cli": "^0.6.8",
+ "@backstage/cli": "^0.6.9",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md
index 94e9960e35..ed87e4dbc5 100644
--- a/plugins/catalog-backend/CHANGELOG.md
+++ b/plugins/catalog-backend/CHANGELOG.md
@@ -1,5 +1,63 @@
# @backstage/plugin-catalog-backend
+## 0.8.1
+
+### Patch Changes
+
+- a99e0bc42: Entity lifecycle and owner are now indexed by the `DefaultCatalogCollator`. A `locationTemplate` may now optionally be provided to its constructor to reflect a custom catalog entity path in the Backstage frontend.
+- Updated dependencies [e1e757569]
+ - @backstage/plugin-search-backend-node@0.1.4
+
+## 0.8.0
+
+### Minor Changes
+
+- 5fe62f124: Fix the schema / code mismatch in LDAP `set` config
+
+### Patch Changes
+
+- 09b5fcf2e: GithubDiscoveryProcessor now excludes archived repositories so they won't be added to Backstage.
+- c2306f898: Externalize repository processing for BitbucketDiscoveryProcessor.
+
+ Add an extension point where you can customize how a matched Bitbucket repository should
+ be processed. This can for example be used if you want to generate the catalog-info.yaml
+ automatically based on other files in a repository, while taking advantage of the
+ build-in repository crawling functionality.
+
+ `BitbucketDiscoveryProcessor.fromConfig` now takes an optional parameter `options.parser` where
+ you can customize the logic for each repository found. The default parser has the same
+ behaviour as before, where it emits an optional location for the matched repository
+ and lets the other processors take care of further processing.
+
+ ```typescript
+ const customRepositoryParser: BitbucketRepositoryParser = async function* customRepositoryParser({
+ client,
+ repository,
+ }) {
+ // Custom logic for interpret the matching repository.
+ // See defaultRepositoryParser for an example
+ };
+
+ const processor = BitbucketDiscoveryProcessor.fromConfig(env.config, {
+ parser: customRepositoryParser,
+ logger: env.logger,
+ });
+ ```
+
+- Updated dependencies [94da20976]
+- Updated dependencies [b9b2b4b76]
+- Updated dependencies [d8cc7e67a]
+- Updated dependencies [99fbef232]
+- Updated dependencies [ab07d77f6]
+- Updated dependencies [d367f63b5]
+- Updated dependencies [937ed39ce]
+- Updated dependencies [b42531cfe]
+- Updated dependencies [9a9e7a42f]
+- Updated dependencies [50ce875a0]
+ - @backstage/core@0.7.6
+ - @backstage/plugin-search-backend-node@0.1.3
+ - @backstage/backend-common@0.6.3
+
## 0.7.1
### Patch Changes
diff --git a/plugins/catalog-backend/migrationsv2/20200511113813_init.js b/plugins/catalog-backend/migrationsv2/20200511113813_init.js
new file mode 100644
index 0000000000..7f3d75e35c
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20200511113813_init.js
@@ -0,0 +1,133 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ return (
+ knex.schema
+ //
+ // locations
+ //
+ .createTable('locations', table => {
+ table.comment(
+ 'Registered locations that shall be contiuously scanned for catalog item updates',
+ );
+ table
+ .uuid('id')
+ .primary()
+ .notNullable()
+ .comment('Auto-generated ID of the location');
+ table.string('type').notNullable().comment('The type of location');
+ table
+ .string('target')
+ .notNullable()
+ .comment('The actual target of the location');
+ })
+ //
+ // entities
+ //
+ .createTable('entities', table => {
+ table.comment('All entities currently stored in the catalog');
+ table.uuid('id').primary().comment('Auto-generated ID of the entity');
+ table
+ .uuid('location_id')
+ .references('id')
+ .inTable('locations')
+ .nullable()
+ .comment('The location that originated the entity');
+ table
+ .string('etag')
+ .notNullable()
+ .comment(
+ 'An opaque string that changes for each update operation to any part of the entity, including metadata.',
+ );
+ table
+ .string('generation')
+ .notNullable()
+ .unsigned()
+ .comment(
+ 'A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes.',
+ );
+ table
+ .string('api_version')
+ .notNullable()
+ .comment('The apiVersion field of the entity');
+ table
+ .string('kind')
+ .notNullable()
+ .comment('The kind field of the entity');
+ table
+ .string('name')
+ .nullable()
+ .comment('The metadata.name field of the entity');
+ table
+ .string('namespace')
+ .nullable()
+ .comment('The metadata.namespace field of the entity');
+ table
+ .string('metadata')
+ .notNullable()
+ .comment('The entire metadata JSON blob of the entity');
+ table
+ .string('spec')
+ .nullable()
+ .comment('The entire spec JSON blob of the entity');
+ })
+ .alterTable('entities', table => {
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
+ table.unique(['kind', 'name', 'namespace'], 'entities_unique_name');
+ })
+ //
+ // entities_search
+ //
+ .createTable('entities_search', table => {
+ table.comment(
+ 'Flattened key-values from the entities, used for quick filtering',
+ );
+ table
+ .uuid('entity_id')
+ .references('id')
+ .inTable('entities')
+ .onDelete('CASCADE')
+ .comment('The entity that matches this key/value');
+ table
+ .string('key')
+ .notNullable()
+ .comment('A key that occurs in the entity');
+ table
+ .string('value')
+ .nullable()
+ .comment('The corresponding value to match on');
+ })
+ );
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ return knex.schema
+ .dropTable('entities_search')
+ .alterTable('entities', table => {
+ table.dropUnique([], 'entities_unique_name');
+ })
+ .dropTable('entities')
+ .dropTable('locations');
+};
diff --git a/plugins/catalog-backend/migrationsv2/20200520140700_location_update_log_table.js b/plugins/catalog-backend/migrationsv2/20200520140700_location_update_log_table.js
new file mode 100644
index 0000000000..d8093fc9b4
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20200520140700_location_update_log_table.js
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ return knex.schema.createTable('location_update_log', table => {
+ table.uuid('id').primary();
+ table.enum('status', ['success', 'fail']).notNullable();
+ table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable();
+ table.string('message');
+ table
+ .uuid('location_id')
+ .references('id')
+ .inTable('locations')
+ .onUpdate('CASCADE')
+ .onDelete('CASCADE');
+ table.string('entity_name').nullable();
+ });
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ return knex.schema.dropTableIfExists('location_update_log');
+};
diff --git a/plugins/catalog-backend/migrationsv2/20200527114117_location_update_log_latest_view.js b/plugins/catalog-backend/migrationsv2/20200527114117_location_update_log_latest_view.js
new file mode 100644
index 0000000000..a0f0f33a65
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20200527114117_location_update_log_latest_view.js
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ // Get list sorted by created_at timestamp in descending order
+ // Grouped by location_id
+ return knex.schema.raw(`
+ CREATE VIEW location_update_log_latest AS
+ SELECT t1.* FROM location_update_log t1
+ JOIN
+ (
+ SELECT location_id, MAX(created_at) AS MAXDATE
+ FROM location_update_log
+ GROUP BY location_id
+ ) t2
+ ON t1.location_id = t2.location_id
+ AND t1.created_at = t2.MAXDATE
+ ORDER BY created_at DESC;
+ `);
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ return knex.schema.raw(`DROP VIEW location_update_log_latest;`);
+};
diff --git a/plugins/catalog-backend/migrationsv2/20200702153613_entities.js b/plugins/catalog-backend/migrationsv2/20200702153613_entities.js
new file mode 100644
index 0000000000..0f1c204f9b
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20200702153613_entities.js
@@ -0,0 +1,236 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ // SQLite does not support FK and PK
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities_search', table => {
+ table.dropForeign(['entity_id']);
+ });
+ await knex.schema.alterTable('entities', table => {
+ table.dropPrimary('entities_pkey');
+ });
+ }
+ await knex.schema.alterTable('entities', table => {
+ table.dropUnique([], 'entities_unique_name');
+ });
+ // Setup temporary tables
+ await knex.schema.renameTable('entities_search', 'tmp_entities_search');
+ await knex.schema.renameTable('entities', 'tmp_entities');
+
+ //
+ // entities
+ //
+ await knex.schema
+ .createTable('entities', table => {
+ table.comment('All entities currently stored in the catalog');
+ table.uuid('id').primary().comment('Auto-generated ID of the entity');
+ table
+ .uuid('location_id')
+ .references('id')
+ .inTable('locations')
+ .nullable()
+ .comment('The location that originated the entity');
+ table
+ .string('etag')
+ .notNullable()
+ .comment(
+ 'An opaque string that changes for each update operation to any part of the entity, including metadata.',
+ );
+ table
+ .string('generation')
+ .notNullable()
+ .unsigned()
+ .comment(
+ 'A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes.',
+ );
+ table
+ .string('api_version')
+ .notNullable()
+ .comment('The apiVersion field of the entity');
+ table
+ .string('kind')
+ .notNullable()
+ .comment('The kind field of the entity');
+ table
+ .string('name')
+ .nullable()
+ .comment('The metadata.name field of the entity');
+ table
+ .string('namespace')
+ .nullable()
+ .comment('The metadata.namespace field of the entity');
+ table
+ .text('metadata')
+ .notNullable()
+ .comment('The entire metadata JSON blob of the entity');
+ table
+ .text('spec')
+ .nullable()
+ .comment('The entire spec JSON blob of the entity');
+ })
+ .alterTable('entities', table => {
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
+ table.unique(['kind', 'name', 'namespace'], 'entities_unique_name');
+ });
+
+ await knex.schema.raw(`INSERT INTO entities SELECT * FROM tmp_entities`);
+
+ //
+ // entities_search
+ //
+ await knex.schema.createTable('entities_search', table => {
+ table.comment(
+ 'Flattened key-values from the entities, used for quick filtering',
+ );
+ table
+ .uuid('entity_id')
+ .references('id')
+ .inTable('entities')
+ .onDelete('CASCADE')
+ .comment('The entity that matches this key/value');
+ table
+ .string('key')
+ .notNullable()
+ .comment('A key that occurs in the entity');
+ table
+ .string('value')
+ .nullable()
+ .comment('The corresponding value to match on');
+ });
+ await knex.schema.raw(
+ `INSERT INTO entities_search SELECT * FROM tmp_entities_search`,
+ );
+
+ // Clean up
+ await knex.schema.dropTable('tmp_entities');
+ return knex.schema.dropTable('tmp_entities_search');
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ // SQLite does not support FK and PK
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities_search', table => {
+ table.dropForeign(['entity_id']);
+ });
+ await knex.schema.alterTable('entities', table => {
+ table.dropPrimary('entities_pkey');
+ });
+ }
+ await knex.schema.alterTable('entities', table => {
+ table.dropUnique([], 'entities_unique_name');
+ });
+
+ // Setup temporary tables
+ await knex.schema.renameTable('entities_search', 'tmp_entities_search');
+ await knex.schema.renameTable('entities', 'tmp_entities');
+
+ //
+ // entities
+ //
+ await knex.schema
+ .createTable('entities', table => {
+ table.comment('All entities currently stored in the catalog');
+ table.uuid('id').primary().comment('Auto-generated ID of the entity');
+ table
+ .uuid('location_id')
+ .references('id')
+ .inTable('locations')
+ .nullable()
+ .comment('The location that originated the entity');
+ table
+ .string('etag')
+ .notNullable()
+ .comment(
+ 'An opaque string that changes for each update operation to any part of the entity, including metadata.',
+ );
+ table
+ .string('generation')
+ .notNullable()
+ .unsigned()
+ .comment(
+ 'A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes.',
+ );
+ table
+ .string('api_version')
+ .notNullable()
+ .comment('The apiVersion field of the entity');
+ table
+ .string('kind')
+ .notNullable()
+ .comment('The kind field of the entity');
+ table
+ .string('name')
+ .nullable()
+ .comment('The metadata.name field of the entity');
+ table
+ .string('namespace')
+ .nullable()
+ .comment('The metadata.namespace field of the entity');
+ table
+ .string('metadata')
+ .notNullable()
+ .comment('The entire metadata JSON blob of the entity');
+ table
+ .string('spec')
+ .nullable()
+ .comment('The entire spec JSON blob of the entity');
+ })
+ .alterTable('entities', table => {
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
+ table.unique(['kind', 'name', 'namespace'], 'entities_unique_name');
+ });
+
+ await knex.schema.raw(`INSERT INTO entities SELECT * FROM tmp_entities`);
+
+ //
+ // entities_search
+ //
+ await knex.schema.createTable('entities_search', table => {
+ table.comment(
+ 'Flattened key-values from the entities, used for quick filtering',
+ );
+ table
+ .uuid('entity_id')
+ .references('id')
+ .inTable('entities')
+ .onDelete('CASCADE')
+ .comment('The entity that matches this key/value');
+ table
+ .string('key')
+ .notNullable()
+ .comment('A key that occurs in the entity');
+ table
+ .string('value')
+ .nullable()
+ .comment('The corresponding value to match on');
+ });
+ await knex.schema.raw(
+ `INSERT INTO entities_search SELECT * FROM tmp_entities_search`,
+ );
+
+ // Clean up
+ await knex.schema.dropTable('tmp_entities');
+ return knex.schema.dropTable('tmp_entities_search');
+};
diff --git a/plugins/catalog-backend/migrationsv2/20200721115244_location_update_log_latest_deduplicate.js b/plugins/catalog-backend/migrationsv2/20200721115244_location_update_log_latest_deduplicate.js
new file mode 100644
index 0000000000..87b41a80fc
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20200721115244_location_update_log_latest_deduplicate.js
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = function up(knex) {
+ return knex.schema.raw(`DROP VIEW location_update_log_latest;`).raw(`
+ CREATE VIEW location_update_log_latest AS
+ SELECT t1.* FROM location_update_log t1
+ JOIN
+ (
+ SELECT location_id, MAX(created_at) AS MAXDATE
+ FROM location_update_log
+ GROUP BY location_id
+ ) t2
+ ON t1.location_id = t2.location_id
+ AND t1.created_at = t2.MAXDATE
+ GROUP BY t1.location_id, t1.id
+ ORDER BY created_at DESC;
+`);
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = function down(knex) {
+ knex.schema.raw(`DROP VIEW location_update_log_latest;`);
+};
diff --git a/plugins/catalog-backend/migrationsv2/20200805163904_location_update_log_duplication_fix.js b/plugins/catalog-backend/migrationsv2/20200805163904_location_update_log_duplication_fix.js
new file mode 100644
index 0000000000..de2b194cff
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20200805163904_location_update_log_duplication_fix.js
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = function up(knex) {
+ return knex.schema
+ .raw('DROP VIEW location_update_log_latest;')
+ .dropTable('location_update_log')
+ .createTable('location_update_log', table => {
+ table.bigIncrements('id').primary(); // instead of uuid, so we can MAX it
+ table.enum('status', ['success', 'fail']).notNullable();
+ table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable();
+ table.string('message');
+ table
+ .uuid('location_id')
+ .references('id')
+ .inTable('locations')
+ .onUpdate('CASCADE')
+ .onDelete('CASCADE');
+ table.string('entity_name').nullable();
+ }).raw(`
+ CREATE VIEW location_update_log_latest AS
+ SELECT t1.* FROM location_update_log t1
+ JOIN
+ (
+ SELECT location_id, MAX(id) AS MAXID
+ FROM location_update_log
+ GROUP BY location_id
+ ) t2
+ ON t1.location_id = t2.location_id
+ AND t1.id = t2.MAXID
+ GROUP BY t1.location_id, t1.id
+ ORDER BY created_at DESC;
+ `);
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = function down(knex) {
+ return knex.schema
+ .raw('DROP VIEW location_update_log_latest;')
+ .dropTable('location_update_log')
+ .createTable('location_update_log', table => {
+ table.uuid('id').primary();
+ table.enum('status', ['success', 'fail']).notNullable();
+ table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable();
+ table.string('message');
+ table
+ .uuid('location_id')
+ .references('id')
+ .inTable('locations')
+ .onUpdate('CASCADE')
+ .onDelete('CASCADE');
+ table.string('entity_name').nullable();
+ }).raw(`
+ CREATE VIEW location_update_log_latest AS
+ SELECT t1.* FROM location_update_log t1
+ JOIN
+ (
+ SELECT location_id, MAX(created_at) AS MAXDATE
+ FROM location_update_log
+ GROUP BY location_id
+ ) t2
+ ON t1.location_id = t2.location_id
+ AND t1.created_at = t2.MAXDATE
+ GROUP BY t1.location_id, t1.id
+ ORDER BY created_at DESC;
+ `);
+};
diff --git a/plugins/catalog-backend/migrationsv2/20200807120600_entitySearch.js b/plugins/catalog-backend/migrationsv2/20200807120600_entitySearch.js
new file mode 100644
index 0000000000..45226e53b4
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20200807120600_entitySearch.js
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ // Sqlite does not support alter column.
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities_search', table => {
+ table.text('value').nullable().alter();
+ });
+ }
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ // Sqlite does not support alter column.
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities_search', table => {
+ table.string('value').nullable().alter();
+ });
+ }
+};
diff --git a/plugins/catalog-backend/migrationsv2/20200809202832_add_bootstrap_location.js b/plugins/catalog-backend/migrationsv2/20200809202832_add_bootstrap_location.js
new file mode 100644
index 0000000000..a90813fe85
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20200809202832_add_bootstrap_location.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ // Adds a single 'bootstrap' location that can be used to trigger work in processors.
+ // This is primarily here to fulfill foreign key constraints.
+ await knex('locations').insert({
+ id: require('uuid').v4(),
+ type: 'bootstrap',
+ target: 'bootstrap',
+ });
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ await knex('locations')
+ .where({
+ type: 'bootstrap',
+ target: 'bootstrap',
+ })
+ .del();
+};
diff --git a/plugins/catalog-backend/migrationsv2/20200923104503_case_insensitivity.js b/plugins/catalog-backend/migrationsv2/20200923104503_case_insensitivity.js
new file mode 100644
index 0000000000..ea5ba9e58d
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20200923104503_case_insensitivity.js
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ await knex('entities')
+ .where({ namespace: null })
+ .update({ namespace: 'default' });
+ await knex('entities_search').update({
+ key: knex.raw('LOWER(key)'),
+ value: knex.raw('LOWER(value)'),
+ });
+};
+
+exports.down = async function down() {};
diff --git a/plugins/catalog-backend/migrationsv2/20201005122705_add_entity_full_name.js b/plugins/catalog-backend/migrationsv2/20201005122705_add_entity_full_name.js
new file mode 100644
index 0000000000..aae1861658
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20201005122705_add_entity_full_name.js
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ await knex.schema.alterTable('entities', table => {
+ table.text('full_name').nullable();
+ });
+
+ await knex('entities').update({
+ full_name: knex.raw(
+ "LOWER(kind) || ':' || LOWER(COALESCE(namespace, 'default')) || '/' || LOWER(name)",
+ ),
+ });
+
+ // SQLite does not support alter column
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities', table => {
+ table.text('full_name').notNullable().alter();
+ });
+ }
+
+ await knex.schema.alterTable('entities', table => {
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
+ table.unique(['full_name'], 'entities_unique_full_name');
+ table.dropUnique([], 'entities_unique_name');
+ });
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('entities', table => {
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
+ table.dropUnique([], 'entities_unique_full_name');
+ table.unique(['kind', 'namespace', 'name'], 'entities_unique_name');
+ });
+
+ await knex.schema.alterTable('entities_search', table => {
+ table.dropColumn('full_name');
+ });
+};
diff --git a/plugins/catalog-backend/migrationsv2/20201006130744_entity_data_column.js b/plugins/catalog-backend/migrationsv2/20201006130744_entity_data_column.js
new file mode 100644
index 0000000000..a8964efbf6
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20201006130744_entity_data_column.js
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ await knex.schema.alterTable('entities', table => {
+ table
+ .text('data')
+ .nullable()
+ .comment('The entire JSON data blob of the entity');
+ });
+
+ await knex('entities').update({
+ // apiVersion and kind should not contain any JSON unsafe chars, and both
+ // metadata and spec are already valid serialized JSON
+ data: knex.raw(
+ `'{"apiVersion":"' || api_version || '","kind":"' || kind || '","metadata":' || metadata || COALESCE(',"spec":' || spec, '') || '}'`,
+ ),
+ });
+
+ await knex.schema.alterTable('entities', table => {
+ table.dropColumn('metadata');
+ table.dropColumn('spec');
+ });
+
+ // SQLite does not support ALTER COLUMN.
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities', table => {
+ table.text('data').notNullable().alter();
+ });
+ }
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('entities', table => {
+ table
+ .text('metadata')
+ .notNullable()
+ .comment('The entire metadata JSON blob of the entity');
+ table
+ .text('spec')
+ .nullable()
+ .comment('The entire spec JSON blob of the entity');
+ table.dropColumn('data');
+ });
+};
diff --git a/plugins/catalog-backend/migrationsv2/20201006203131_entity_remove_redundant_columns.js b/plugins/catalog-backend/migrationsv2/20201006203131_entity_remove_redundant_columns.js
new file mode 100644
index 0000000000..f40df5f73e
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20201006203131_entity_remove_redundant_columns.js
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ await knex.schema.alterTable('entities', table => {
+ table.dropColumn('api_version');
+ table.dropColumn('kind');
+ table.dropColumn('name');
+ table.dropColumn('namespace');
+ });
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('entities', table => {
+ table
+ .string('api_version')
+ .notNullable()
+ .comment('The apiVersion field of the entity');
+ table.string('kind').notNullable().comment('The kind field of the entity');
+ table
+ .string('name')
+ .nullable()
+ .comment('The metadata.name field of the entity');
+ table
+ .string('namespace')
+ .nullable()
+ .comment('The metadata.namespace field of the entity');
+ });
+};
diff --git a/plugins/catalog-backend/migrationsv2/20201007201501_index_entity_search.js b/plugins/catalog-backend/migrationsv2/20201007201501_index_entity_search.js
new file mode 100644
index 0000000000..77bf0529eb
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20201007201501_index_entity_search.js
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ await knex.schema.alterTable('entities_search', table => {
+ table.index(['key'], 'entities_search_key');
+ table.index(['value'], 'entities_search_value');
+ });
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('entities_search', table => {
+ table.dropIndex('', 'entities_search_key');
+ table.dropIndex('', 'entities_search_value');
+ });
+};
diff --git a/plugins/catalog-backend/migrationsv2/20201019130742_add_relations_table.js b/plugins/catalog-backend/migrationsv2/20201019130742_add_relations_table.js
new file mode 100644
index 0000000000..85e729f814
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20201019130742_add_relations_table.js
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ await knex.schema.createTable('entities_relations', table => {
+ table.comment('All relations between entities in the catalog');
+ table
+ .uuid('originating_entity_id')
+ .references('id')
+ .inTable('entities')
+ .onDelete('CASCADE')
+ .notNullable()
+ .comment('The entity that provided the relation');
+ table
+ .string('source_full_name')
+ .notNullable()
+ .comment('The full name of the source entity of the relation');
+ table
+ .string('type')
+ .notNullable()
+ .comment('The type of the relation between the entities');
+ table
+ .string('target_full_name')
+ .notNullable()
+ .comment('The full name of the target entity of the relation');
+
+ table.primary(['source_full_name', 'type', 'target_full_name']);
+ });
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ await knex.schema.dropTable('entities_relations');
+};
diff --git a/plugins/catalog-backend/migrationsv2/20201123205611_relations_table_uniq.js b/plugins/catalog-backend/migrationsv2/20201123205611_relations_table_uniq.js
new file mode 100644
index 0000000000..9e8198b5eb
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20201123205611_relations_table_uniq.js
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ if (knex.client.config.client === 'sqlite3') {
+ // sqlite doesn't support dropPrimary so we recreate it properly instead
+ await knex.schema.dropTable('entities_relations');
+ await knex.schema.createTable('entities_relations', table => {
+ table.comment('All relations between entities in the catalog');
+ table
+ .uuid('originating_entity_id')
+ .references('id')
+ .inTable('entities')
+ .onDelete('CASCADE')
+ .notNullable()
+ .comment('The entity that provided the relation');
+ table
+ .string('source_full_name')
+ .notNullable()
+ .comment('The full name of the source entity of the relation');
+ table
+ .string('type')
+ .notNullable()
+ .comment('The type of the relation between the entities');
+ table
+ .string('target_full_name')
+ .notNullable()
+ .comment('The full name of the target entity of the relation');
+ table.index('source_full_name', 'source_full_name_idx');
+ });
+ } else {
+ await knex.schema.alterTable('entities_relations', table => {
+ table.dropPrimary();
+ table.index('source_full_name', 'source_full_name_idx');
+ });
+ }
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ if (knex.client.config.client === 'sqlite3') {
+ await knex.schema.dropTable('entities_relations');
+ await knex.schema.createTable('entities_relations', table => {
+ table.comment('All relations between entities in the catalog');
+ table
+ .uuid('originating_entity_id')
+ .references('id')
+ .inTable('entities')
+ .onDelete('CASCADE')
+ .notNullable()
+ .comment('The entity that provided the relation');
+ table
+ .string('source_full_name')
+ .notNullable()
+ .comment('The full name of the source entity of the relation');
+ table
+ .string('type')
+ .notNullable()
+ .comment('The type of the relation between the entities');
+ table
+ .string('target_full_name')
+ .notNullable()
+ .comment('The full name of the target entity of the relation');
+
+ table.primary(['source_full_name', 'type', 'target_full_name']);
+ });
+ } else {
+ await knex.schema.alterTable('entities_relations', table => {
+ table.dropIndex([], 'source_full_name_idx');
+ table.primary(['source_full_name', 'type', 'target_full_name']);
+ });
+ }
+};
diff --git a/plugins/catalog-backend/migrationsv2/20201210185851_fk_index.js b/plugins/catalog-backend/migrationsv2/20201210185851_fk_index.js
new file mode 100644
index 0000000000..abb26cd5fc
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20201210185851_fk_index.js
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities_relations', table => {
+ table.index('originating_entity_id', 'originating_entity_id_idx');
+ });
+ await knex.schema.alterTable('entities_search', table => {
+ table.index('entity_id', 'entity_id_idx');
+ });
+ }
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities_relations', table => {
+ table.dropIndex([], 'originating_entity_id_idx');
+ });
+ await knex.schema.alterTable('entities_relations', table => {
+ table.dropIndex([], 'entity_id_idx');
+ });
+ }
+};
diff --git a/plugins/catalog-backend/migrationsv2/20201230103504_update_log_varchar.js b/plugins/catalog-backend/migrationsv2/20201230103504_update_log_varchar.js
new file mode 100644
index 0000000000..d924b0414a
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20201230103504_update_log_varchar.js
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ if (knex.client.config.client !== 'sqlite3') {
+ // We actually just want to widen columns, but can't do that while a
+ // view is dependent on them - so we just reconstruct it exactly as it was
+ await knex.schema
+ .raw('DROP VIEW location_update_log_latest;')
+ .alterTable('location_update_log', table => {
+ table.text('message').alter();
+ table.text('entity_name').nullable().alter();
+ }).raw(`
+ CREATE VIEW location_update_log_latest AS
+ SELECT t1.* FROM location_update_log t1
+ JOIN
+ (
+ SELECT location_id, MAX(id) AS MAXID
+ FROM location_update_log
+ GROUP BY location_id
+ ) t2
+ ON t1.location_id = t2.location_id
+ AND t1.id = t2.MAXID
+ GROUP BY t1.location_id, t1.id
+ ORDER BY created_at DESC;
+ `);
+ }
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema
+ .raw('DROP VIEW location_update_log_latest;')
+ .alterTable('location_update_log', table => {
+ table.string('message').alter();
+ table.string('entity_name').nullable().alter();
+ }).raw(`
+ CREATE VIEW location_update_log_latest AS
+ SELECT t1.* FROM location_update_log t1
+ JOIN
+ (
+ SELECT location_id, MAX(id) AS MAXID
+ FROM location_update_log
+ GROUP BY location_id
+ ) t2
+ ON t1.location_id = t2.location_id
+ AND t1.id = t2.MAXID
+ GROUP BY t1.location_id, t1.id
+ ORDER BY created_at DESC;
+ `);
+ }
+};
diff --git a/plugins/catalog-backend/migrationsv2/20210209121210_locations_fk_index.js b/plugins/catalog-backend/migrationsv2/20210209121210_locations_fk_index.js
new file mode 100644
index 0000000000..ccfb1faffb
--- /dev/null
+++ b/plugins/catalog-backend/migrationsv2/20210209121210_locations_fk_index.js
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities', table => {
+ table.index('location_id', 'entity_location_id_idx');
+ });
+ await knex.schema.alterTable('location_update_log', table => {
+ table.index('location_id', 'update_log_location_id_idx');
+ });
+ }
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities', table => {
+ table.dropIndex([], 'entity_location_id_idx');
+ });
+ await knex.schema.alterTable('location_update_log', table => {
+ table.dropIndex([], 'update_log_location_id_idx');
+ });
+ }
+};
diff --git a/plugins/catalog-backend/migrationsv2/20210302150147_refresh_state.js b/plugins/catalog-backend/migrationsv2/20210302150147_refresh_state.js
index 6abab6270b..4c03405910 100644
--- a/plugins/catalog-backend/migrationsv2/20210302150147_refresh_state.js
+++ b/plugins/catalog-backend/migrationsv2/20210302150147_refresh_state.js
@@ -33,7 +33,6 @@ exports.up = async function up(knex) {
);
table
.text('entity_ref')
- .unique()
.notNullable()
.comment('A reference to the entity that the refresh state is tied to');
table
@@ -59,13 +58,15 @@ exports.up = async function up(knex) {
.notNullable()
.comment('JSON array containing all errors related to entity');
table
- .dateTime('next_update_at')
+ .dateTime('next_update_at') // TODO: timezone or change to epoch-millis or similar
.notNullable()
.comment('Timestamp of when entity should be updated');
table
- .dateTime('last_discovery_at')
+ .dateTime('last_discovery_at') // TODO: timezone or change to epoch-millis or similar
.notNullable()
.comment('The last timestamp of which this entity was discovered');
+ table.unique(['entity_ref'], 'refresh_state_entity_ref_uniq');
+ table.index('entity_id', 'refresh_state_entity_id_idx');
table.index('entity_ref', 'refresh_state_entity_ref_idx');
table.index('next_update_at', 'refresh_state_next_update_at_idx');
});
@@ -82,10 +83,18 @@ exports.up = async function up(knex) {
.inTable('refresh_state')
.onDelete('CASCADE')
.comment(
- 'Entity ID which correspond to the ID in the refresh_state table',
+ 'Entity ID which corresponds to the ID in the refresh_state table',
);
- table.text('etag').notNullable().comment('Etag to be used for caching');
- table.text('finalized_entity').notNullable().comment('The final entity');
+ table
+ .text('hash')
+ .notNullable()
+ .comment(
+ 'Stable hash of the entity data, to be used for caching and avoiding redundant work',
+ );
+ table
+ .text('final_entity')
+ .notNullable()
+ .comment('The JSON encoded final entity');
table.index('entity_id', 'final_entities_entity_id_idx');
});
@@ -94,38 +103,38 @@ exports.up = async function up(knex) {
'Holds edges between refresh state rows. Every time when an entity is processed and emits another entity, an edge will be stored to represent that fact. This is used to detect orphans and ultimately deletions.',
);
table
- .text('source_special_key')
+ .increments('id')
+ .comment('Primary key to distinguish unique lines from each other');
+ table
+ .text('source_key')
.nullable()
.comment(
'When the reference source is not an entity, this is an opaque identifier for that source.',
);
table
- .text('source_entity_id')
+ .text('source_entity_ref')
.nullable()
- .references('entity_id')
+ .references('entity_ref')
.inTable('refresh_state')
.onDelete('CASCADE')
.comment(
- 'When the reference source is an entity, this is the ID of the source entity.',
+ 'When the reference source is an entity, this is the EntityRef of the source entity.',
);
table
- .text('target_entity_id')
+ .text('target_entity_ref')
.notNullable()
- .references('entity_id')
+ .references('entity_ref')
.inTable('refresh_state')
.onDelete('CASCADE')
- .comment('The ID of the target entity.');
+ .comment('The EntityRef of the target entity.');
+ table.index('source_key', 'refresh_state_references_source_key_idx');
table.index(
- 'source_special_key',
- 'refresh_state_references_source_special_key_idx',
+ 'source_entity_ref',
+ 'refresh_state_references_source_entity_ref_idx',
);
table.index(
- 'source_entity_id',
- 'refresh_state_references_source_entity_id_idx',
- );
- table.index(
- 'target_entity_id',
- 'refresh_state_references_target_entity_id_idx',
+ 'target_entity_ref',
+ 'refresh_state_references_target_entity_ref_idx',
);
});
@@ -153,6 +162,29 @@ exports.up = async function up(knex) {
table.index('source_entity_ref', 'relations_source_entity_ref_idx');
table.index('originating_entity_id', 'relations_source_entity_id_idx');
});
+
+ await knex.schema.createTable('search', table => {
+ table.comment(
+ 'Flattened key-values from the entities, used for quick filtering',
+ );
+ table
+ .text('entity_id')
+ .references('entity_id')
+ .inTable('refresh_state')
+ .onDelete('CASCADE')
+ .comment('The entity that matches this key/value');
+ table
+ .string('key')
+ .notNullable()
+ .comment('A key that occurs in the entity');
+ table
+ .string('value')
+ .nullable()
+ .comment('The corresponding value to match on');
+ table.index(['entity_id'], 'search_entity_id_idx');
+ table.index(['key'], 'search_key_idx');
+ table.index(['value'], 'search_value_idx');
+ });
};
/**
@@ -165,6 +197,8 @@ exports.down = async function down(knex) {
table.dropIndex([], 'refresh_state_references_target_entity_id_idx');
});
await knex.schema.alterTable('refresh_state', table => {
+ table.dropUnique([], 'refresh_state_entity_ref_uniq');
+ table.dropIndex([], 'refresh_state_entity_id_idx');
table.dropIndex([], 'refresh_state_entity_ref_idx');
table.dropIndex([], 'refresh_state_next_update_at_idx');
});
@@ -175,6 +209,13 @@ exports.down = async function down(knex) {
table.index('source_entity_ref', 'relations_source_entity_ref_idx');
table.index('originating_entity_id', 'relations_source_entity_id_idx');
});
+ await knex.schema.alterTable('search', table => {
+ table.dropIndex([], 'search_entity_id_idx');
+ table.dropIndex([], 'search_key_idx');
+ table.dropIndex([], 'search_value_idx');
+ });
+
+ await knex.schema.dropTable('search');
await knex.schema.dropTable('final_entities');
await knex.schema.dropTable('relations');
await knex.schema.dropTable('references');
diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json
index 5125e9703e..7649067a44 100644
--- a/plugins/catalog-backend/package.json
+++ b/plugins/catalog-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
- "version": "0.7.1",
+ "version": "0.8.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -30,13 +30,12 @@
},
"dependencies": {
"@azure/msal-node": "^1.0.0-beta.3",
- "@backstage/backend-common": "^0.6.1",
+ "@backstage/backend-common": "^0.6.3",
"@backstage/catalog-model": "^0.7.7",
"@backstage/config": "^0.1.4",
- "@backstage/core": "^0.7.3",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
- "@backstage/plugin-search-backend-node": "^0.1.2",
+ "@backstage/plugin-search-backend-node": "^0.1.4",
"@backstage/search-common": "^0.1.1",
"@octokit/graphql": "^4.5.8",
"@types/express": "^4.17.6",
@@ -61,11 +60,10 @@
"winston": "^3.2.1",
"yaml": "^1.9.2",
"yn": "^4.0.0",
- "yup": "^0.29.3",
- "zen-observable": "^0.8.15"
+ "yup": "^0.29.3"
},
"devDependencies": {
- "@backstage/cli": "^0.6.8",
+ "@backstage/cli": "^0.6.9",
"@backstage/test-utils": "^0.1.9",
"@types/core-js": "^2.5.4",
"@types/git-url-parse": "^9.0.0",
@@ -80,7 +78,6 @@
"files": [
"dist",
"migrations/**/*.{js,d.ts}",
- "migrationsv2/**/*.{js,d.ts}",
"config.d.ts"
],
"configSchema": "config.d.ts"
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts
index aac9a091b8..607581e153 100644
--- a/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts
@@ -88,7 +88,7 @@ describe('readLdapConfig', () => {
filter: 'f',
paged: true,
},
- set: [{ path: 'p', value: 'v' }],
+ set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
@@ -107,7 +107,7 @@ describe('readLdapConfig', () => {
filter: 'f',
paged: true,
},
- set: [{ path: 'p', value: 'v' }],
+ set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
@@ -136,7 +136,7 @@ describe('readLdapConfig', () => {
filter: 'f',
paged: true,
},
- set: [{ path: 'p', value: 'v' }],
+ set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
@@ -155,7 +155,7 @@ describe('readLdapConfig', () => {
filter: 'f',
paged: true,
},
- set: [{ path: 'p', value: 'v' }],
+ set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
@@ -182,9 +182,9 @@ describe('readLdapConfig', () => {
dn: 'udn',
options: {
filter: `
- (|
- (cn=foo bar)
- (cn=bar)
+ (|
+ (cn=foo bar)
+ (cn=bar)
)
`,
},
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts
index 625282ca05..aca02e6e10 100644
--- a/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts
@@ -57,7 +57,7 @@ export type UserConfig = {
// default is scope "one" and attributes "*" and "+".
options: SearchOptions;
// JSON paths (on a.b.c form) and hard coded values to set on those paths
- set?: { path: string; value: JsonValue }[];
+ set?: { [path: string]: JsonValue };
// Mappings from well known entity fields, to LDAP attribute names
map: {
// The name of the attribute that holds the relative distinguished name of
@@ -94,7 +94,7 @@ export type GroupConfig = {
// Only the scope, filter, attributes, and paged fields are supported.
options: SearchOptions;
// JSON paths (on a.b.c form) and hard coded values to set on those paths
- set?: { path: string; value: JsonValue }[];
+ set?: { [path: string]: JsonValue };
// Mappings from well known entity fields, to LDAP attribute names
map: {
// The name of the attribute that holds the relative distinguished name of
@@ -189,15 +189,12 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
}
function readSetConfig(
- c: Config[] | undefined,
- ): { path: string; value: JsonValue }[] | undefined {
+ c: Config | undefined,
+ ): { [path: string]: JsonValue } | undefined {
if (!c) {
return undefined;
}
- return c.map(entry => ({
- path: entry.getString('path'),
- value: entry.get('value'),
- }));
+ return Object.fromEntries(c.keys().map(path => [path, c.get(path)]));
}
function readUserMapConfig(
@@ -244,7 +241,7 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
return {
dn: c.getString('dn'),
options: readOptionsConfig(c.getOptionalConfig('options')),
- set: readSetConfig(c.getOptionalConfigArray('set')),
+ set: readSetConfig(c.getOptionalConfig('set')),
map: readUserMapConfig(c.getOptionalConfig('map')),
};
}
@@ -255,7 +252,7 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
return {
dn: c.getString('dn'),
options: readOptionsConfig(c.getOptionalConfig('options')),
- set: readSetConfig(c.getOptionalConfigArray('set')),
+ set: readSetConfig(c.getOptionalConfig('set')),
map: readGroupMapConfig(c.getOptionalConfig('map')),
};
}
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts
index 3162fbd01c..68542b5b30 100644
--- a/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts
@@ -15,6 +15,7 @@
*/
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
+import { SearchEntry } from 'ldapjs';
import lodashSet from 'lodash/set';
import { buildOrgHierarchy } from '../util/org';
import { LdapClient } from './client';
@@ -25,7 +26,6 @@ import {
LDAP_UUID_ANNOTATION,
} from './constants';
import { LdapVendor } from './vendors';
-import { SearchEntry } from 'ldapjs';
/**
* Reads users out of an LDAP provider.
@@ -63,7 +63,7 @@ export async function readLdapUsers(
};
if (set) {
- for (const { path, value } of set) {
+ for (const [path, value] of Object.entries(set)) {
lodashSet(entity, path, value);
}
}
@@ -142,7 +142,7 @@ export async function readLdapGroups(
};
if (set) {
- for (const { path, value } of set) {
+ for (const [path, value] of Object.entries(set)) {
lodashSet(entity, path, value);
}
}
diff --git a/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts b/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts
index 4d6f67de22..1d8e139fb3 100644
--- a/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts
@@ -38,7 +38,7 @@ describe('parseEntityYaml', () => {
spec:
type: website
lifecycle: production
- owner: guest
+ owner: user:guest
`,
'utf8',
),
@@ -60,7 +60,7 @@ describe('parseEntityYaml', () => {
spec: {
type: 'website',
lifecycle: 'production',
- owner: 'guest',
+ owner: 'user:guest',
},
}),
]);
diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts
new file mode 100644
index 0000000000..226e5c06a8
--- /dev/null
+++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { resolvePackagePath } from '@backstage/backend-common';
+import { ConfigReader } from '@backstage/config';
+import path from 'path';
+import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider';
+import { EntityProviderConnection } from './types';
+
+describe('ConfigLocationEntityProvider', () => {
+ it('should apply mutation with the correct paths in the config', async () => {
+ const mockConfig = new ConfigReader({
+ catalog: {
+ locations: [
+ { type: 'file', target: './lols.yaml' },
+ { type: 'url', target: 'https://github.com/backstage/backstage' },
+ ],
+ },
+ });
+
+ const mockConnection = ({
+ applyMutation: jest.fn(),
+ } as unknown) as EntityProviderConnection;
+ const locationProvider = new ConfigLocationEntityProvider(mockConfig);
+
+ await locationProvider.connect(mockConnection);
+
+ expect(mockConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'full',
+ entities: expect.arrayContaining([
+ expect.objectContaining({
+ spec: {
+ target: path.join(
+ resolvePackagePath('@backstage/plugin-catalog-backend'),
+ './lols.yaml',
+ ),
+ type: 'file',
+ },
+ }),
+ ]),
+ });
+ expect(mockConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'full',
+ entities: expect.arrayContaining([
+ expect.objectContaining({
+ spec: {
+ target: 'https://github.com/backstage/backstage',
+ type: 'url',
+ },
+ }),
+ ]),
+ });
+ });
+});
diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts
new file mode 100644
index 0000000000..bccbd0ba41
--- /dev/null
+++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Config } from '@backstage/config';
+import path from 'path';
+import { EntityProvider, EntityProviderConnection } from './types';
+import { locationSpecToLocationEntity } from './util';
+
+export class ConfigLocationEntityProvider implements EntityProvider {
+ private connection: EntityProviderConnection | undefined;
+
+ constructor(private readonly config: Config) {}
+
+ getProviderName(): string {
+ return 'ConfigLocationProvider';
+ }
+
+ async connect(connection: EntityProviderConnection): Promise {
+ this.connection = connection;
+
+ const locationConfigs =
+ this.config.getOptionalConfigArray('catalog.locations') ?? [];
+
+ const entities = locationConfigs.map(location => {
+ const type = location.getString('type');
+ const target = location.getString('target');
+ return locationSpecToLocationEntity({
+ type,
+ target: type === 'file' ? path.resolve(target) : target,
+ });
+ });
+
+ await this.connection.applyMutation({
+ type: 'full',
+ entities,
+ });
+ }
+}
diff --git a/plugins/catalog-backend/src/next/Context/BackgroundContext.ts b/plugins/catalog-backend/src/next/Context/BackgroundContext.ts
new file mode 100644
index 0000000000..72b9a3b1ed
--- /dev/null
+++ b/plugins/catalog-backend/src/next/Context/BackgroundContext.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Context, ContextKey } from './types';
+
+/**
+ * A base Context implementation that does not hold any value.
+ */
+export class BackgroundContext implements Context {
+ getContextValue(key: ContextKey): T {
+ return key.defaultValue;
+ }
+}
diff --git a/plugins/catalog-backend/src/next/Context/ContextWithValue.ts b/plugins/catalog-backend/src/next/Context/ContextWithValue.ts
new file mode 100644
index 0000000000..e8f94fd922
--- /dev/null
+++ b/plugins/catalog-backend/src/next/Context/ContextWithValue.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Context, ContextKey } from './types';
+
+/**
+ * A Context implementation that holds a single value, optionally extending an existing context.
+ */
+export class ContextWithValue implements Context {
+ static create(parent: Context, key: ContextKey, value: unknown) {
+ return new ContextWithValue(parent, key, value);
+ }
+
+ private constructor(
+ private readonly parent: Context,
+ private readonly key: ContextKey,
+ private readonly value: unknown,
+ ) {}
+
+ getContextValue(key: ContextKey): T {
+ if (this.key === key) {
+ return this.value as T;
+ }
+ return this.parent.getContextValue(key);
+ }
+}
diff --git a/plugins/catalog-backend/src/next/Context/TransactionValue.ts b/plugins/catalog-backend/src/next/Context/TransactionValue.ts
new file mode 100644
index 0000000000..6959d13a18
--- /dev/null
+++ b/plugins/catalog-backend/src/next/Context/TransactionValue.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Context, ContextKey } from './types';
+import { Knex } from 'knex';
+import { ContextWithValue } from './ContextWithValue';
+
+const transactionContextKey = new ContextKey(
+ undefined,
+);
+
+/**
+ * TransactionValue handles the wrapping of a knex transaction in a Context.
+ */
+export class TransactionValue {
+ static in(parent: Context, tx: Knex.Transaction) {
+ return ContextWithValue.create(parent, transactionContextKey, tx);
+ }
+
+ static from(context: Context): Knex.Transaction {
+ const transaction = context.getContextValue(transactionContextKey);
+ if (!transaction) {
+ throw new Error(`No transaction available in context`);
+ }
+ return transaction;
+ }
+}
diff --git a/plugins/catalog-backend/src/next/Context/index.ts b/plugins/catalog-backend/src/next/Context/index.ts
new file mode 100644
index 0000000000..61dd4a2958
--- /dev/null
+++ b/plugins/catalog-backend/src/next/Context/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { BackgroundContext } from './BackgroundContext';
+export { ContextWithValue } from './ContextWithValue';
+export { TransactionValue } from './TransactionValue';
+export { ContextKey } from './types';
+export type { Context } from './types';
diff --git a/plugins/catalog-backend/src/next/Context/types.ts b/plugins/catalog-backend/src/next/Context/types.ts
new file mode 100644
index 0000000000..0973b83515
--- /dev/null
+++ b/plugins/catalog-backend/src/next/Context/types.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class ContextKey {
+ constructor(readonly defaultValue: T) {}
+}
+
+export interface Context {
+ getContextValue(key: ContextKey): T;
+}
diff --git a/plugins/catalog-backend/src/next/DatabaseLocationProvider.ts b/plugins/catalog-backend/src/next/DatabaseLocationProvider.ts
deleted file mode 100644
index eaf5a78556..0000000000
--- a/plugins/catalog-backend/src/next/DatabaseLocationProvider.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2021 Spotify AB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Observable } from '@backstage/core';
-import { ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
-import { EntityProvider, LocationStore, EntityMessage } from './types';
-import ObservableImpl from 'zen-observable';
-import {
- locationSpecToLocationEntity,
- locationSpecToMetadataName,
-} from './util';
-
-export class DatabaseLocationProvider implements EntityProvider {
- private subscribers = new Set<
- ZenObservable.SubscriptionObserver
- >();
-
- constructor(private readonly store: LocationStore) {
- store.location$().subscribe({
- next: locations => {
- if ('all' in locations) {
- this.notify({
- all: locations.all.map(l => locationSpecToLocationEntity(l)),
- });
- } else {
- this.notify({
- added: locations.added.map(l => locationSpecToLocationEntity(l)),
- removed: locations.removed.map(l => ({
- kind: 'Location',
- namespace: ENTITY_DEFAULT_NAMESPACE,
- name: locationSpecToMetadataName(l),
- })),
- });
- }
- },
- });
- }
-
- private notify(message: EntityMessage) {
- for (const subscriber of this.subscribers) {
- subscriber.next(message);
- }
- }
-
- entityChange$(): Observable {
- return new ObservableImpl(subscriber => {
- this.store.listLocations().then(locations => {
- subscriber.next({
- all: locations.map(l => locationSpecToLocationEntity(l)),
- });
- this.subscribers.add(subscriber);
- });
- return () => {
- this.subscribers.delete(subscriber);
- };
- });
- }
-}
diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts
index 934ab3a1cc..e724fe79d8 100644
--- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts
+++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts
@@ -14,21 +14,47 @@
* limitations under the License.
*/
-import { Subscription } from '@backstage/core';
+import { stringifyEntityRef } from '@backstage/catalog-model';
+import { Logger } from 'winston';
+import { Stitcher } from './Stitcher';
import {
CatalogProcessingEngine,
- EntityProvider,
- EntityMessage,
- ProcessingStateManager,
CatalogProcessingOrchestrator,
+ EntityProvider,
+ EntityProviderConnection,
+ EntityProviderMutation,
+ ProcessingStateManager,
} from './types';
-import { Logger } from 'winston';
-import { stringifyEntityRef } from '@backstage/catalog-model';
-import { Stitcher } from './Stitcher';
+class Connection implements EntityProviderConnection {
+ constructor(
+ private readonly config: {
+ stateManager: ProcessingStateManager;
+ id: string;
+ },
+ ) {}
+
+ async applyMutation(mutation: EntityProviderMutation): Promise {
+ if (mutation.type === 'full') {
+ await this.config.stateManager.replaceProcessingItems({
+ sourceKey: this.config.id,
+ type: 'full',
+ items: mutation.entities,
+ });
+
+ return;
+ }
+
+ await this.config.stateManager.replaceProcessingItems({
+ sourceKey: this.config.id,
+ type: 'delta',
+ added: mutation.added,
+ removed: mutation.removed,
+ });
+ }
+}
export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
- private subscriptions: Subscription[] = [];
private running: boolean = false;
constructor(
@@ -41,11 +67,12 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
async start() {
for (const provider of this.entityProviders) {
- const id = 'databaseProvider';
- const subscription = provider
- .entityChange$()
- .subscribe({ next: m => this.onNext(id, m) });
- this.subscriptions.push(subscription);
+ await provider.connect(
+ new Connection({
+ stateManager: this.stateManager,
+ id: provider.getProviderName(),
+ }),
+ );
}
this.running = true;
@@ -54,12 +81,12 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
const {
id,
entity,
- state: intialState,
+ state: initialState,
} = await this.stateManager.getNextProcessingItem();
const result = await this.orchestrator.process({
entity,
- state: intialState,
+ state: initialState,
});
for (const error of result.errors) {
@@ -77,7 +104,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
state: result.state,
errors: result.errors,
relations: result.relations,
- deferredEntities: result.deferredEntites,
+ deferredEntities: result.deferredEntities,
});
const setOfThingsToStitch = new Set([
@@ -92,30 +119,5 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
async stop() {
this.running = false;
-
- for (const subscription of this.subscriptions) {
- subscription.unsubscribe();
- }
- }
-
- private async onNext(id: string, message: EntityMessage) {
- if ('all' in message) {
- // TODO unhandled rejection
- await this.stateManager.addProcessingItems({
- id,
- type: 'provider',
- entities: message.all,
- });
- }
-
- if ('added' in message) {
- await this.stateManager.addProcessingItems({
- id,
- type: 'provider',
- entities: message.added,
- });
-
- // TODO deletions of message.removed
- }
}
}
diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingOrchestrator.ts
index 1880b5d17e..8fbb19e536 100644
--- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingOrchestrator.ts
+++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingOrchestrator.ts
@@ -179,7 +179,7 @@ export class DefaultCatalogProcessingOrchestrator
);
}
- // Backwards compatible processing of location entites
+ // Backwards compatible processing of location entities
if (isLocationEntity(entity)) {
const { type = location.type } = entity.spec;
const targets = new Array();
@@ -268,7 +268,7 @@ function createEmitter(logger: Logger, parentEntity: Entity) {
const errors = new Array();
const relations = new Array();
- const deferredEntites = new Array();
+ const deferredEntities = new Array();
const emit = (i: CatalogProcessorResult) => {
if (done) {
@@ -282,7 +282,7 @@ function createEmitter(logger: Logger, parentEntity: Entity) {
if (i.type === 'entity') {
const originLocation = getEntityOriginLocationRef(parentEntity);
- deferredEntites.push({
+ deferredEntities.push({
...i.entity,
metadata: {
...i.entity.metadata,
@@ -294,7 +294,7 @@ function createEmitter(logger: Logger, parentEntity: Entity) {
},
});
} else if (i.type === 'location') {
- deferredEntites.push(
+ deferredEntities.push(
locationSpecToLocationEntity(i.location, parentEntity),
);
} else if (i.type === 'relation') {
@@ -311,7 +311,7 @@ function createEmitter(logger: Logger, parentEntity: Entity) {
return {
errors,
relations,
- deferredEntites,
+ deferredEntities,
};
},
};
diff --git a/plugins/catalog-backend/src/next/DefaultService.ts b/plugins/catalog-backend/src/next/DefaultLocationService.ts
similarity index 100%
rename from plugins/catalog-backend/src/next/DefaultService.ts
rename to plugins/catalog-backend/src/next/DefaultLocationService.ts
diff --git a/plugins/catalog-backend/src/next/DefaultLocationStore.test.ts b/plugins/catalog-backend/src/next/DefaultLocationStore.test.ts
new file mode 100644
index 0000000000..f9bb97477c
--- /dev/null
+++ b/plugins/catalog-backend/src/next/DefaultLocationStore.test.ts
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { v4 as uuid } from 'uuid';
+import { DatabaseManager } from './database/DatabaseManager';
+import { DefaultLocationStore } from './DefaultLocationStore';
+
+/* eslint-disable */
+xdescribe('DefaultLocationStore', () => {
+ const createLocationStore = async () => {
+ const db = await DatabaseManager.createTestDatabase();
+ const connection = { applyMutation: jest.fn() };
+ const store = new DefaultLocationStore(db);
+ await store.connect(connection);
+ return { store, connection };
+ };
+
+ it('should do a full sync with the locations on connect', async () => {
+ const { connection } = await createLocationStore();
+
+ expect(connection.applyMutation).toHaveBeenCalledWith({
+ type: 'full',
+ entities: [],
+ });
+ });
+
+ describe('listLocations', () => {
+ it('lists empty locations when there is no locations', async () => {
+ const { store } = await createLocationStore();
+
+ expect(await store.listLocations()).toEqual([]);
+ });
+
+ it('lists locations that are added to the db', async () => {
+ const { store } = await createLocationStore();
+
+ await store.createLocation({
+ target:
+ 'https://github.com/backstage/demo/blob/master/catalog-info.yml',
+ type: 'url',
+ });
+
+ const listLocations = await store.listLocations();
+
+ expect(listLocations).toHaveLength(1);
+ expect(listLocations).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ target:
+ 'https://github.com/backstage/demo/blob/master/catalog-info.yml',
+ type: 'url',
+ }),
+ ]),
+ );
+ });
+ });
+
+ describe('createLocation', () => {
+ it('throws when the location already exists', async () => {
+ const { store } = await createLocationStore();
+ const spec = {
+ target:
+ 'https://github.com/backstage/demo/blob/master/catalog-info.yml',
+ type: 'url',
+ };
+ await store.createLocation(spec);
+
+ await expect(() => store.createLocation(spec)).rejects.toThrow(
+ new RegExp(`Location ${spec.type}:${spec.target} already exists`),
+ );
+ });
+
+ it('calls apply mutation when adding a new location', async () => {
+ const { store, connection } = await createLocationStore();
+
+ await store.createLocation({
+ target:
+ 'https://github.com/backstage/demo/blob/master/catalog-info.yml',
+ type: 'url',
+ });
+
+ expect(connection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ removed: [],
+ added: expect.arrayContaining([
+ expect.objectContaining({
+ spec: {
+ target:
+ 'https://github.com/backstage/demo/blob/master/catalog-info.yml',
+ type: 'url',
+ },
+ }),
+ ]),
+ });
+ });
+ });
+
+ describe('deleteLocation', () => {
+ it('throws if the location does not exist', async () => {
+ const { store } = await createLocationStore();
+
+ const id = uuid();
+
+ await expect(() => store.deleteLocation(id)).rejects.toThrow(
+ new RegExp(`Found no location with ID ${id}`),
+ );
+ });
+
+ it('calls apply mutation when adding a new location', async () => {
+ const { store, connection } = await createLocationStore();
+
+ const location = await store.createLocation({
+ target:
+ 'https://github.com/backstage/demo/blob/master/catalog-info.yml',
+ type: 'url',
+ });
+
+ await store.deleteLocation(location.id);
+
+ expect(connection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [],
+ removed: expect.arrayContaining([
+ expect.objectContaining({
+ spec: {
+ target:
+ 'https://github.com/backstage/demo/blob/master/catalog-info.yml',
+ type: 'url',
+ },
+ }),
+ ]),
+ });
+ });
+ });
+});
diff --git a/plugins/catalog-backend/src/next/DefaultLocationStore.ts b/plugins/catalog-backend/src/next/DefaultLocationStore.ts
index b21fe9da02..f6267df6a5 100644
--- a/plugins/catalog-backend/src/next/DefaultLocationStore.ts
+++ b/plugins/catalog-backend/src/next/DefaultLocationStore.ts
@@ -16,27 +16,26 @@
import { LocationSpec, Location } from '@backstage/catalog-model';
import { Database } from '../database';
-import { LocationStore } from './types';
-import { v4 as uuidv4 } from 'uuid';
+import {
+ LocationStore,
+ EntityProvider,
+ EntityProviderConnection,
+} from './types';
+import { v4 as uuid } from 'uuid';
+import { locationSpecToLocationEntity } from './util';
import { ConflictError } from '@backstage/errors';
-import { Observable } from '@backstage/core';
-import ObservableImpl from 'zen-observable';
-export type LocationMessage =
- | { all: Location[] }
- | { added: Location[]; removed: Location[] };
-
-export class DefaultLocationStore implements LocationStore {
- private subscribers = new Set<
- ZenObservable.SubscriptionObserver
- >();
+export class DefaultLocationStore implements LocationStore, EntityProvider {
+ private _connection: EntityProviderConnection | undefined;
constructor(private readonly db: Database) {}
- createLocation(spec: LocationSpec): Promise {
- return this.db.transaction(async tx => {
- // TODO: id should really be type and target combined and not a uuid.
+ getProviderName(): string {
+ return 'DefaultLocationStore';
+ }
+ async createLocation(spec: LocationSpec): Promise {
+ return this.db.transaction(async tx => {
// Attempt to find a previous location matching the spec
const previousLocations = await this.listLocations();
const previousLocation = previousLocations.some(
@@ -49,13 +48,18 @@ export class DefaultLocationStore implements LocationStore {
);
}
+ // TODO: id should really be type and target combined and not a uuid.
const location = await this.db.addLocation(tx, {
- id: uuidv4(),
+ id: uuid(),
type: spec.type,
target: spec.target,
});
- this.notifyAddition(location);
+ await this.connection.applyMutation({
+ type: 'delta',
+ added: [locationSpecToLocationEntity(location)],
+ removed: [],
+ });
return location;
});
@@ -63,11 +67,17 @@ export class DefaultLocationStore implements LocationStore {
async listLocations(): Promise {
const dbLocations = await this.db.locations();
- return dbLocations.map(item => ({
- id: item.id,
- target: item.target,
- type: item.type,
- }));
+ return (
+ dbLocations
+ // TODO(blam): We should create a mutation to remove this location for everyone
+ // eventually when it's all done and dusted
+ .filter(({ type }) => type !== 'bootstrap')
+ .map(item => ({
+ id: item.id,
+ target: item.target,
+ type: item.type,
+ }))
+ );
}
getLocation(id: string): Promise {
@@ -75,40 +85,38 @@ export class DefaultLocationStore implements LocationStore {
}
deleteLocation(id: string): Promise {
+ if (!this.connection) {
+ throw new Error('location store is not initialized');
+ }
+
return this.db.transaction(async tx => {
const location = await this.db.location(id);
- if (!location) {
- throw new ConflictError(`No location found with id: ${id}`);
- }
await this.db.removeLocation(tx, id);
- this.notifyDeletion(location);
+ await this.connection.applyMutation({
+ type: 'delta',
+ added: [],
+ removed: [locationSpecToLocationEntity(location)],
+ });
});
}
- private notifyAddition(location: Location) {
- for (const subscriber of this.subscribers) {
- subscriber.next({
- added: [location],
- removed: [],
- });
+ private get connection(): EntityProviderConnection {
+ if (!this._connection) {
+ throw new Error('location store is not initialized');
}
+
+ return this._connection;
}
- private notifyDeletion(location: Location) {
- for (const subscriber of this.subscribers) {
- subscriber.next({
- added: [],
- removed: [location],
- });
- }
- }
-
- location$(): Observable {
- return new ObservableImpl(subscriber => {
- this.subscribers.add(subscriber);
- return () => {
- this.subscribers.delete(subscriber);
- };
+ async connect(connection: EntityProviderConnection): Promise {
+ this._connection = connection;
+ const locations = await this.listLocations();
+ const entities = locations.map(location => {
+ return locationSpecToLocationEntity(location);
+ });
+ await this.connection.applyMutation({
+ type: 'full',
+ entities,
});
}
}
diff --git a/plugins/catalog-backend/src/next/DefaultProcessingStateManager.ts b/plugins/catalog-backend/src/next/DefaultProcessingStateManager.ts
index 91090bc139..9c1d20ae9c 100644
--- a/plugins/catalog-backend/src/next/DefaultProcessingStateManager.ts
+++ b/plugins/catalog-backend/src/next/DefaultProcessingStateManager.ts
@@ -14,17 +14,25 @@
* limitations under the License.
*/
-import { ProcessingDatabase, RefreshStateItem } from './database/types';
+import { ProcessingDatabase } from './database/types';
import {
- AddProcessingItemRequest,
- ProccessingItem,
+ ProcessingItem,
ProcessingItemResult,
ProcessingStateManager,
+ ReplaceProcessingItemsRequest,
} from './types';
export class DefaultProcessingStateManager implements ProcessingStateManager {
constructor(private readonly db: ProcessingDatabase) {}
+ replaceProcessingItems(
+ request: ReplaceProcessingItemsRequest,
+ ): Promise {
+ return this.db.transaction(async tx => {
+ await this.db.replaceUnprocessedEntities(tx, request);
+ });
+ }
+
async setProcessingItemResult(result: ProcessingItemResult) {
return this.db.transaction(async tx => {
await this.db.updateProcessedEntity(tx, {
@@ -38,37 +46,24 @@ export class DefaultProcessingStateManager implements ProcessingStateManager {
});
}
- async addProcessingItems(request: AddProcessingItemRequest) {
- return this.db.transaction(async tx => {
- await this.db.addUnprocessedEntities(tx, request);
- });
- }
-
- async getNextProcessingItem(): Promise {
- const entities = await new Promise(resolve =>
- this.popFromQueue(resolve),
- );
- const { id, state, unprocessedEntity } = entities[0];
- return {
- id,
- entity: unprocessedEntity,
- state,
- };
- }
-
- async popFromQueue(resolve: (rows: RefreshStateItem[]) => void) {
- const entities = await this.db.transaction(async tx => {
- return this.db.getProcessableEntities(tx, {
- processBatchSize: 1,
+ async getNextProcessingItem(): Promise {
+ for (;;) {
+ const { items } = await this.db.transaction(async tx => {
+ return this.db.getProcessableEntities(tx, {
+ processBatchSize: 1,
+ });
});
- });
- // No entities require refresh, wait and try again.
- if (!entities.items.length) {
- setTimeout(() => this.popFromQueue(resolve), 1000);
- return;
+ if (items.length) {
+ const { id, state, unprocessedEntity } = items[0];
+ return {
+ id,
+ entity: unprocessedEntity,
+ state,
+ };
+ }
+
+ await new Promise(resolve => setTimeout(resolve, 1000));
}
-
- resolve(entities.items);
}
}
diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts
index c3e0a6bd9c..10500fdc15 100644
--- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts
+++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts
@@ -19,7 +19,6 @@ import {
resolvePackagePath,
UrlReader,
} from '@backstage/backend-common';
-import fs from 'fs-extra';
import {
DefaultNamespaceEntityPolicy,
EntityPolicies,
@@ -39,6 +38,7 @@ import {
EntitiesCatalog,
LocationsCatalog,
} from '../catalog';
+import { CommonDatabase } from '../database/CommonDatabase';
import {
AnnotateLocationEntityProcessor,
BitbucketDiscoveryProcessor,
@@ -53,7 +53,6 @@ import {
MicrosoftGraphOrgReaderProcessor,
PlaceholderProcessor,
PlaceholderResolver,
- StaticLocationProcessor,
UrlReaderProcessor,
} from '../ingestion';
import { RepoLocationAnalyzer } from '../ingestion/LocationAnalyzer';
@@ -64,16 +63,15 @@ import {
} from '../ingestion/processors/PlaceholderProcessor';
import { defaultEntityDataParser } from '../ingestion/processors/util/parse';
import { LocationAnalyzer } from '../ingestion/types';
+import { CatalogProcessingEngine } from '../next/types';
+import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider';
+import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine';
import { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator';
-import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
-import { DatabaseLocationProvider } from '../next/DatabaseLocationProvider';
import { DefaultLocationStore } from './DefaultLocationStore';
import { DefaultProcessingStateManager } from './DefaultProcessingStateManager';
-import { CatalogProcessingEngine } from '../next/types';
import { NextEntitiesCatalog } from './NextEntitiesCatalog';
import { Stitcher } from './Stitcher';
-import { CommonDatabase } from '../database/CommonDatabase';
export type CatalogEnvironment = {
logger: Logger;
@@ -244,19 +242,13 @@ export class NextCatalogBuilder {
const parser = this.parser || defaultEntityDataParser;
const dbClient = await database.getClient();
- const allMigrations = resolvePackagePath(
- '@backstage/plugin-catalog-backend',
- 'migrations',
- );
-
- const migrationsDir = resolvePackagePath(
- '@backstage/plugin-catalog-backend',
- 'migrationsv2',
- );
- await fs.copy(allMigrations, migrationsDir);
await dbClient.migrate.latest({
- directory: migrationsDir,
+ directory: resolvePackagePath(
+ '@backstage/plugin-catalog-backend',
+ 'migrationsv2',
+ ),
});
+
const db = new CommonDatabase(dbClient, logger);
const processingDatabase = new DefaultProcessingDatabase(dbClient, logger);
@@ -272,11 +264,11 @@ export class NextCatalogBuilder {
const entitiesCatalog = new NextEntitiesCatalog(dbClient);
const locationStore = new DefaultLocationStore(db);
- const dbLocationProvider = new DatabaseLocationProvider(locationStore);
const stitcher = new Stitcher(dbClient, logger);
+ const configLocationProvider = new ConfigLocationEntityProvider(config);
const processingEngine = new DefaultCatalogProcessingEngine(
logger,
- [dbLocationProvider], // entityproviders
+ [locationStore, configLocationProvider],
stateManager,
orchestrator,
stitcher,
@@ -324,7 +316,6 @@ export class NextCatalogBuilder {
// These are always there no matter what
const processors: CatalogProcessor[] = [
- StaticLocationProcessor.fromConfig(config),
new PlaceholderProcessor({ resolvers: placeholderResolvers, reader }),
new BuiltinKindsEntityProcessor(),
];
@@ -340,7 +331,6 @@ export class NextCatalogBuilder {
MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }),
new UrlReaderProcessor({ reader, logger }),
CodeOwnersProcessor.fromConfig(config, { logger, reader }),
- // new LocationEntityProcessor({ integrations }),
new AnnotateLocationEntityProcessor({ integrations }),
);
}
diff --git a/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts b/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts
index 5bddd47f41..e87bb01e1f 100644
--- a/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts
+++ b/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts
@@ -37,7 +37,7 @@ export class NextEntitiesCatalog implements EntitiesCatalog {
'final_entities',
).select();
- const entities = dbResponse.map(e => JSON.parse(e.finalized_entity));
+ const entities = dbResponse.map(e => JSON.parse(e.final_entity));
return {
entities,
diff --git a/plugins/catalog-backend/src/next/Stitcher.test.ts b/plugins/catalog-backend/src/next/Stitcher.test.ts
new file mode 100644
index 0000000000..f38af384fa
--- /dev/null
+++ b/plugins/catalog-backend/src/next/Stitcher.test.ts
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getVoidLogger } from '@backstage/backend-common';
+import { Knex } from 'knex';
+import { DatabaseManager } from './database/DatabaseManager';
+import {
+ DbRefreshStateReferencesRow,
+ DbRefreshStateRow,
+ DbRelationsRow,
+} from './database/DefaultProcessingDatabase';
+import { DbSearchRow } from './search';
+import { DbFinalEntitiesRow, Stitcher } from './Stitcher';
+
+describe('Stitcher', () => {
+ let db: Knex;
+ const logger = getVoidLogger();
+
+ beforeEach(async () => {
+ db = await DatabaseManager.createTestDatabaseConnection();
+ await DatabaseManager.createDatabase(db);
+ });
+
+ it('runs the happy path', async () => {
+ const stitcher = new Stitcher(db, logger);
+
+ await db.transaction(async tx => {
+ await tx('refresh_state').insert([
+ {
+ entity_id: 'my-id',
+ entity_ref: 'k:ns/n',
+ unprocessed_entity: JSON.stringify({}),
+ processed_entity: JSON.stringify({
+ apiVersion: 'a',
+ kind: 'k',
+ metadata: {
+ name: 'n',
+ namespace: 'ns',
+ },
+ spec: {
+ k: 'v',
+ },
+ }),
+ errors: '[]',
+ next_update_at: tx.fn.now(),
+ last_discovery_at: tx.fn.now(),
+ },
+ ]);
+ await tx('refresh_state_references').insert([
+ { source_key: 'a', target_entity_ref: 'k:ns/n' },
+ ]);
+ await tx('relations').insert([
+ {
+ originating_entity_id: 'my-id',
+ source_entity_ref: 'k:ns/n',
+ type: 'looksAt',
+ target_entity_ref: 'k:ns/other',
+ },
+ ]);
+ });
+
+ await stitcher.stitch(new Set(['k:ns/n']));
+
+ let firstHash: string;
+ await db.transaction(async tx => {
+ const entities = await tx('final_entities');
+
+ expect(entities.length).toBe(1);
+ const entity = JSON.parse(entities[0].final_entity);
+ expect(entity).toEqual({
+ relations: [
+ {
+ type: 'looksAt',
+ target: {
+ kind: 'k',
+ namespace: 'ns',
+ name: 'other',
+ },
+ },
+ ],
+ apiVersion: 'a',
+ kind: 'k',
+ metadata: {
+ name: 'n',
+ namespace: 'ns',
+ etag: expect.any(String),
+ generation: 1,
+ uid: 'my-id',
+ },
+ spec: {
+ k: 'v',
+ },
+ });
+
+ expect(entity.metadata.etag).toEqual(entities[0].hash);
+ firstHash = entities[0].hash;
+
+ const search = await tx('search');
+ expect(search).toEqual(
+ expect.arrayContaining([
+ { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/other' },
+ { entity_id: 'my-id', key: 'apiversion', value: 'a' },
+ { entity_id: 'my-id', key: 'kind', value: 'k' },
+ { entity_id: 'my-id', key: 'metadata.name', value: 'n' },
+ { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' },
+ { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' },
+ { entity_id: 'my-id', key: 'spec.k', value: 'v' },
+ ]),
+ );
+ });
+
+ // Re-stitch without any changes
+ await stitcher.stitch(new Set(['k:ns/n']));
+
+ await db.transaction(async tx => {
+ const entities = await tx('final_entities');
+ expect(entities.length).toBe(1);
+ const entity = JSON.parse(entities[0].final_entity);
+ expect(entities[0].hash).toEqual(firstHash);
+ expect(entity.metadata.etag).toEqual(firstHash);
+ });
+
+ // Now add one more relation and re-stitch
+ await db.transaction(async tx => {
+ await tx('relations').insert([
+ {
+ originating_entity_id: 'my-id',
+ source_entity_ref: 'k:ns/n',
+ type: 'looksAt',
+ target_entity_ref: 'k:ns/third',
+ },
+ ]);
+ });
+
+ await stitcher.stitch(new Set(['k:ns/n']));
+
+ await db.transaction(async tx => {
+ const entities = await tx('final_entities');
+
+ expect(entities.length).toBe(1);
+ const entity = JSON.parse(entities[0].final_entity);
+ expect(entity).toEqual({
+ relations: expect.arrayContaining([
+ {
+ type: 'looksAt',
+ target: {
+ kind: 'k',
+ namespace: 'ns',
+ name: 'other',
+ },
+ },
+ {
+ type: 'looksAt',
+ target: {
+ kind: 'k',
+ namespace: 'ns',
+ name: 'third',
+ },
+ },
+ ]),
+ apiVersion: 'a',
+ kind: 'k',
+ metadata: {
+ name: 'n',
+ namespace: 'ns',
+ etag: expect.any(String),
+ generation: 1,
+ uid: 'my-id',
+ },
+ spec: {
+ k: 'v',
+ },
+ });
+
+ expect(entities[0].hash).not.toEqual(firstHash);
+ expect(entities[0].hash).toEqual(entity.metadata.etag);
+
+ const search = await tx('search');
+ expect(search).toEqual(
+ expect.arrayContaining([
+ { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/other' },
+ { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/third' },
+ { entity_id: 'my-id', key: 'apiversion', value: 'a' },
+ { entity_id: 'my-id', key: 'kind', value: 'k' },
+ { entity_id: 'my-id', key: 'metadata.name', value: 'n' },
+ { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' },
+ { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' },
+ { entity_id: 'my-id', key: 'spec.k', value: 'v' },
+ ]),
+ );
+ });
+ });
+});
diff --git a/plugins/catalog-backend/src/next/Stitcher.ts b/plugins/catalog-backend/src/next/Stitcher.ts
index 3e5e3ac24b..bdc028d923 100644
--- a/plugins/catalog-backend/src/next/Stitcher.ts
+++ b/plugins/catalog-backend/src/next/Stitcher.ts
@@ -14,26 +14,28 @@
* limitations under the License.
*/
+import { Entity, parseEntityRef } from '@backstage/catalog-model';
+import { ConflictError } from '@backstage/errors';
+import { createHash } from 'crypto';
+import stableStringify from 'fast-json-stable-stringify';
import { Knex } from 'knex';
import { Logger } from 'winston';
import { Transaction } from '../database';
-import { ConflictError } from '@backstage/errors';
-import {
- DbRefreshStateReferences,
- DbRefreshStateRow,
- DbRelationsRow,
-} from './database/DefaultProcessingDatabase';
-import { Entity, parseEntityRef } from '@backstage/catalog-model';
-import { createHash } from 'crypto';
-import stableStringify from 'fast-json-stable-stringify';
+import { buildEntitySearch, DbSearchRow } from './search';
+
+// The number of items that are sent per batch to the database layer, when
+// doing .batchInsert calls to knex. This needs to be low enough to not cause
+// errors in the underlying engine due to exceeding query limits, but large
+// enough to get the speed benefits.
+const BATCH_SIZE = 50;
export type DbFinalEntitiesRow = {
entity_id: string;
- etag: string;
- finalized_entity: string;
+ hash: string;
+ final_entity: string;
};
-function generateEntityEtag(entity: Entity) {
+function generateStableHash(entity: Entity) {
return createHash('sha1')
.update(stableStringify({ ...entity }))
.digest('hex');
@@ -49,64 +51,129 @@ export class Stitcher {
for (const entityRef of entityRefs) {
await this.transaction(async txOpaque => {
const tx = txOpaque as Knex.Transaction;
- const [result] = await tx('refresh_state')
- .select('entity_id', 'processed_entity')
- .where({ entity_ref: entityRef });
- if (!result) {
+ // Selecting from refresh_state and final_entities should yield exactly
+ // one row (except in abnormal cases where the stitch was invoked for
+ // something that didn't exist at all, in which case it's zero rows).
+ // The join with the temporary incoming_references still gives one row.
+ // The only result set "expanding" join is the one with relations, so
+ // the output should be at least one row (if zero or one relations were
+ // found), or at most the same number of rows as relations.
+ const result: Array<{
+ entityId: string;
+ processedEntity?: string;
+ errors: string;
+ incomingReferenceCount: string | number;
+ previousHash?: string;
+ relationType?: string;
+ relationTarget?: string;
+ }> = await tx
+ .with('incoming_references', function incomingReferences(builder) {
+ return builder
+ .from('refresh_state_references')
+ .where({ target_entity_ref: entityRef })
+ .count({ count: '*' });
+ })
+ .select({
+ entityId: 'refresh_state.entity_id',
+ processedEntity: 'refresh_state.processed_entity',
+ errors: 'refresh_state.errors',
+ incomingReferenceCount: 'incoming_references.count',
+ previousHash: 'final_entities.hash',
+ relationType: 'relations.type',
+ relationTarget: 'relations.target_entity_ref',
+ })
+ .from('refresh_state')
+ .leftJoin('incoming_references', {})
+ .leftOuterJoin('final_entities', {
+ 'final_entities.entity_id': 'refresh_state.entity_id',
+ })
+ .leftOuterJoin('relations', {
+ 'relations.source_entity_ref': 'refresh_state.entity_ref',
+ })
+ .where({ 'refresh_state.entity_ref': entityRef })
+ .orderBy('relationType', 'asc')
+ .orderBy('relationTarget', 'asc');
+
+ // If there were no rows returned, it would mean that there was no
+ // matching row even in the refresh_state. This can happen for example
+ // if we emit a relation to something that hasn't been ingested yet.
+ // It's safe to ignore this stitch attempt in that case.
+ if (!result.length) {
this.logger.debug(
`Unable to stitch ${entityRef}, item does not exist in refresh state table`,
);
return;
- } else if (!result.processed_entity) {
+ }
+
+ const {
+ entityId,
+ processedEntity,
+ // errors,
+ incomingReferenceCount,
+ previousHash,
+ } = result[0];
+
+ // If there was no processed entity in place, the target hasn't been
+ // through the processing steps yet. It's safe to ignore this stitch
+ // attempt in that case, since another stitch will be triggered when
+ // that processing has finished.
+ if (!processedEntity) {
this.logger.debug(
`Unable to stitch ${entityRef}, the entity has not yet been processed`,
);
return;
}
- const entity: Entity = JSON.parse(result.processed_entity);
+ // Grab the processed entity and stitch all of the relevant data into
+ // it
+ const entity = JSON.parse(processedEntity) as Entity;
+ const isOrphan = Number(incomingReferenceCount) === 0;
- const entityId = entity?.metadata?.uid;
- if (!entityId) {
- this.logger.error(`missing ID in entity ${JSON.stringify(entity)}`);
- return;
- }
-
- const [reference_count_result] = await tx(
- 'refresh_state_references',
- )
- .where({ target_entity_id: entity.metadata.uid })
- .count({ reference_count: 'target_entity_id' });
-
- if (Number(reference_count_result.reference_count) === 0) {
- this.logger.debug(`${entityRef} is orphan`);
+ if (isOrphan) {
+ this.logger.debug(`${entityRef} is an orphan`);
entity.metadata.annotations = {
...entity.metadata.annotations,
['backstage.io/orphan']: 'true',
};
}
- const relationResults = await tx('relations')
- .where({ source_entity_ref: entityRef })
- .select();
+ // TODO: entityRef is lower case and should be uppercase in the final
+ // result
+ entity.relations = result
+ .filter(row => row.relationType /* exclude null row, if relevant */)
+ .map(row => ({
+ type: row.relationType!,
+ target: parseEntityRef(row.relationTarget!),
+ }));
- // TODO: entityRef is lower case and should be uppercase in the final result.
- entity.relations = relationResults.map(relation => ({
- type: relation.type,
- target: parseEntityRef(relation.target_entity_ref),
- }));
+ // If the output entity was actually not changed, just abort
+ const hash = generateStableHash(entity);
+ if (hash === previousHash) {
+ this.logger.debug(`Skipped stitching of ${entityRef}, no changes`);
+ return;
+ }
+
+ entity.metadata.uid = entityId;
entity.metadata.generation = 1;
- const etag = generateEntityEtag(entity);
- entity.metadata.etag = etag;
+ if (!entity.metadata.etag) {
+ // If the original data source did not have its own etag handling,
+ // use the hash as a good-quality etag
+ entity.metadata.etag = hash;
+ }
+
await tx('final_entities')
.insert({
- finalized_entity: JSON.stringify(entity),
entity_id: entityId,
- etag,
+ final_entity: JSON.stringify(entity),
+ hash,
})
.onConflict('entity_id')
- .merge(['finalized_entity', 'etag']);
+ .merge(['final_entity', 'hash']);
+
+ const searchEntries = buildEntitySearch(entityId, entity);
+ await tx('search').where({ entity_id: entityId }).delete();
+ await tx.batchInsert('search', searchEntries, BATCH_SIZE);
});
}
}
diff --git a/plugins/catalog-backend/src/next/database/DatabaseManager.ts b/plugins/catalog-backend/src/next/database/DatabaseManager.ts
new file mode 100644
index 0000000000..22ae4e782f
--- /dev/null
+++ b/plugins/catalog-backend/src/next/database/DatabaseManager.ts
@@ -0,0 +1,99 @@
+/*
+ * 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 { getVoidLogger, resolvePackagePath } from '@backstage/backend-common';
+import knexFactory, { Knex } from 'knex';
+import { v4 as uuid } from 'uuid';
+import { Logger } from 'winston';
+import { CommonDatabase } from '../../database/CommonDatabase';
+import { Database } from '../../database/types';
+
+export type CreateDatabaseOptions = {
+ logger: Logger;
+};
+
+const defaultOptions: CreateDatabaseOptions = {
+ logger: getVoidLogger(),
+};
+
+export class DatabaseManager {
+ public static async createDatabase(
+ knex: Knex,
+ options: Partial = {},
+ ): Promise {
+ const migrationsDir = resolvePackagePath(
+ '@backstage/plugin-catalog-backend',
+ 'migrationsv2',
+ );
+
+ await knex.migrate.latest({
+ directory: migrationsDir,
+ });
+ const { logger } = { ...defaultOptions, ...options };
+ return new CommonDatabase(knex, logger);
+ }
+
+ public static async createInMemoryDatabase(): Promise {
+ const knex = await this.createInMemoryDatabaseConnection();
+ return await this.createDatabase(knex);
+ }
+
+ public static async createInMemoryDatabaseConnection(): Promise {
+ const knex = knexFactory({
+ client: 'sqlite3',
+ connection: ':memory:',
+ useNullAsDefault: true,
+ });
+
+ knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
+ resource.run('PRAGMA foreign_keys = ON', () => {});
+ });
+
+ return knex;
+ }
+
+ public static async createTestDatabase(): Promise {
+ const knex = await this.createTestDatabaseConnection();
+ return await this.createDatabase(knex);
+ }
+
+ public static async createTestDatabaseConnection(): Promise {
+ const config: Knex.Config = {
+ client: 'sqlite3',
+ connection: ':memory:',
+ useNullAsDefault: true,
+ };
+
+ let knex = knexFactory(config);
+ if (typeof config.connection !== 'string') {
+ const tempDbName = `d${uuid().replace(/-/g, '')}`;
+ await knex.raw(`CREATE DATABASE ${tempDbName};`);
+ knex = knexFactory({
+ ...config,
+ connection: {
+ ...config.connection,
+ database: tempDbName,
+ },
+ });
+ }
+
+ knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
+ resource.run('PRAGMA foreign_keys = ON', () => {});
+ });
+
+ return knex;
+ }
+}
diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts
new file mode 100644
index 0000000000..d76bd74260
--- /dev/null
+++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts
@@ -0,0 +1,517 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// import { DefaultProcessingDatabase } from './DefaultProcessingDatabase';
+import { DatabaseManager } from './DatabaseManager';
+import { Knex } from 'knex';
+import {
+ DbRefreshStateReferencesRow,
+ DbRefreshStateRow,
+ DbRelationsRow,
+ DefaultProcessingDatabase,
+} from './DefaultProcessingDatabase';
+
+import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
+import * as uuid from 'uuid';
+import { getVoidLogger } from '@backstage/backend-common';
+
+describe('Default Processing Database', () => {
+ let db: Knex;
+ let processingDatabase: DefaultProcessingDatabase;
+ const logger = getVoidLogger();
+
+ beforeEach(async () => {
+ db = await DatabaseManager.createTestDatabaseConnection();
+ await DatabaseManager.createDatabase(db);
+
+ processingDatabase = new DefaultProcessingDatabase(db, logger);
+ });
+
+ describe('replaceUnprocessedEntities', () => {
+ const insertRefRow = async (ref: DbRefreshStateReferencesRow) => {
+ return db('refresh_state_references').insert(
+ ref,
+ );
+ };
+
+ const insertRefreshStateRow = async (ref: DbRefreshStateRow) => {
+ await db('refresh_state').insert(ref);
+ };
+
+ const createLocations = async (entityRefs: string[]) => {
+ for (const ref of entityRefs) {
+ await insertRefreshStateRow({
+ entity_id: uuid.v4(),
+ entity_ref: ref,
+ unprocessed_entity: '{}',
+ processed_entity: '{}',
+ errors: '',
+ next_update_at: 'now()',
+ last_discovery_at: 'now()',
+ });
+ }
+ };
+
+ it('replaces all existing state correctly for simple dependency chains', async () => {
+ /*
+ config -> location:default/root -> location:default/root-1 -> location:default/root-2
+ database -> location:default/second -> location:default/root-2
+ */
+ await createLocations([
+ 'location:default/root',
+ 'location:default/root-1',
+ 'location:default/root-2',
+ 'location:default/second',
+ ]);
+
+ await insertRefRow({
+ source_key: 'config',
+ target_entity_ref: 'location:default/root',
+ });
+
+ await insertRefRow({
+ source_key: 'database',
+ target_entity_ref: 'location:default/second',
+ });
+
+ await insertRefRow({
+ source_entity_ref: 'location:default/root',
+ target_entity_ref: 'location:default/root-1',
+ });
+
+ await insertRefRow({
+ source_entity_ref: 'location:default/root-1',
+ target_entity_ref: 'location:default/root-2',
+ });
+
+ await insertRefRow({
+ source_entity_ref: 'location:default/second',
+ target_entity_ref: 'location:default/root-2',
+ });
+
+ await processingDatabase.transaction(async tx => {
+ await processingDatabase.replaceUnprocessedEntities(tx, {
+ type: 'full',
+ sourceKey: 'config',
+ items: [
+ {
+ apiVersion: '1.0.0',
+ metadata: {
+ name: 'new-root',
+ },
+ kind: 'Location',
+ } as Entity,
+ ],
+ });
+ });
+
+ const currentRefreshState = await db(
+ 'refresh_state',
+ ).select();
+
+ const currentRefRowState = await db(
+ 'refresh_state_references',
+ ).select();
+
+ for (const ref of ['location:default/root', 'location:default/root-1']) {
+ expect(currentRefreshState.some(t => t.entity_ref === ref)).toBeFalsy();
+ }
+
+ expect(
+ currentRefreshState.some(
+ t => t.entity_ref === 'location:default/new-root',
+ ),
+ ).toBeTruthy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.source_entity_ref === 'location:default/root' &&
+ t.target_entity_ref === 'location:default/root-1',
+ ),
+ ).toBeFalsy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.source_entity_ref === 'location:default/root-1' &&
+ t.target_entity_ref === 'location:default/root-2',
+ ),
+ ).toBeFalsy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.target_entity_ref === 'location:default/root-1' &&
+ t.source_key === 'config',
+ ),
+ ).toBeFalsy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.target_entity_ref === 'location:default/new-root' &&
+ t.source_key === 'config',
+ ),
+ ).toBeTruthy();
+ });
+
+ it('should work for more complex chains', async () => {
+ /*
+ config -> location:default/root -> location:default/root-1 -> location:default/root-2
+ config -> location:default/root -> location:default/root-1a -> location:default/root-2
+ */
+ await createLocations([
+ 'location:default/root',
+ 'location:default/root-1',
+ 'location:default/root-2',
+ 'location:default/root-1a',
+ ]);
+
+ await insertRefRow({
+ source_key: 'config',
+ target_entity_ref: 'location:default/root',
+ });
+
+ await insertRefRow({
+ source_entity_ref: 'location:default/root',
+ target_entity_ref: 'location:default/root-1',
+ });
+
+ await insertRefRow({
+ source_entity_ref: 'location:default/root',
+ target_entity_ref: 'location:default/root-1a',
+ });
+
+ await insertRefRow({
+ source_entity_ref: 'location:default/root-1',
+ target_entity_ref: 'location:default/root-2',
+ });
+
+ await insertRefRow({
+ source_entity_ref: 'location:default/root-1a',
+ target_entity_ref: 'location:default/root-2',
+ });
+
+ await processingDatabase.transaction(async tx => {
+ await processingDatabase.replaceUnprocessedEntities(tx, {
+ type: 'full',
+ sourceKey: 'config',
+ items: [
+ {
+ apiVersion: '1.0.0',
+ metadata: {
+ name: 'new-root',
+ },
+ kind: 'Location',
+ } as Entity,
+ ],
+ });
+ });
+
+ const currentRefreshState = await db(
+ 'refresh_state',
+ ).select();
+
+ const currentRefRowState = await db(
+ 'refresh_state_references',
+ ).select();
+
+ const deletedRefs = [
+ 'location:default/root',
+ 'location:default/root-1',
+ 'location:default/root-1a',
+ 'location:default/root-2',
+ ];
+
+ for (const ref of deletedRefs) {
+ expect(currentRefreshState.some(t => t.entity_ref === ref)).toBeFalsy();
+ }
+
+ expect(
+ currentRefreshState.some(
+ t => t.entity_ref === 'location:default/new-root',
+ ),
+ ).toBeTruthy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.source_key === 'config' &&
+ t.target_entity_ref === 'location:default/new-root',
+ ),
+ ).toBeTruthy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.source_key === 'config' &&
+ t.target_entity_ref === 'location:default/root',
+ ),
+ ).toBeFalsy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.source_entity_ref === 'location:default/root' &&
+ t.target_entity_ref === 'location:default/root-1',
+ ),
+ ).toBeFalsy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.source_entity_ref === 'location:default/root' &&
+ t.target_entity_ref === 'location:default/root-1a',
+ ),
+ ).toBeFalsy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.source_entity_ref === 'location:default/root-1' &&
+ t.target_entity_ref === 'location:default/root-2',
+ ),
+ ).toBeFalsy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.source_entity_ref === 'location:default/root-1a' &&
+ t.target_entity_ref === 'location:default/root-2',
+ ),
+ ).toBeFalsy();
+ });
+
+ it('should add new locations using the delta options', async () => {
+ await processingDatabase.transaction(async tx => {
+ await processingDatabase.replaceUnprocessedEntities(tx, {
+ type: 'delta',
+ sourceKey: 'lols',
+ removed: [],
+ added: [
+ {
+ apiVersion: '1.0.0',
+ metadata: {
+ name: 'new-root',
+ },
+ kind: 'Location',
+ } as Entity,
+ ],
+ });
+ });
+
+ const currentRefreshState = await db(
+ 'refresh_state',
+ ).select();
+
+ const currentRefRowState = await db(
+ 'refresh_state_references',
+ ).select();
+
+ expect(
+ currentRefreshState.some(
+ t => t.entity_ref === 'location:default/new-root',
+ ),
+ ).toBeTruthy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.source_key === 'lols' &&
+ t.target_entity_ref === 'location:default/new-root',
+ ),
+ ).toBeTruthy();
+ });
+
+ it('should not remove locations that are referenced elsewhere', async () => {
+ /*
+ config-1 -> location:default/root
+ config-2 -> location:default/root
+ */
+ await createLocations(['location:default/root']);
+
+ await insertRefRow({
+ source_key: 'config-1',
+ target_entity_ref: 'location:default/root',
+ });
+ await insertRefRow({
+ source_key: 'config-2',
+ target_entity_ref: 'location:default/root',
+ });
+
+ await processingDatabase.transaction(async tx => {
+ await processingDatabase.replaceUnprocessedEntities(tx, {
+ type: 'full',
+ sourceKey: 'config-1',
+ items: [],
+ });
+ });
+
+ const currentRefreshState = await db(
+ 'refresh_state',
+ ).select();
+
+ const currentRefRowState = await db(
+ 'refresh_state_references',
+ ).select();
+
+ expect(currentRefRowState).toEqual([
+ expect.objectContaining({
+ source_key: 'config-2',
+ target_entity_ref: 'location:default/root',
+ }),
+ ]);
+
+ expect(currentRefreshState).toEqual([
+ expect.objectContaining({
+ entity_ref: 'location:default/root',
+ }),
+ ]);
+ });
+
+ it('should remove old locations using the delta options', async () => {
+ await createLocations(['location:default/new-root']);
+
+ await insertRefRow({
+ source_key: 'lols',
+ target_entity_ref: 'location:default/new-root',
+ });
+
+ await processingDatabase.transaction(async tx => {
+ await processingDatabase.replaceUnprocessedEntities(tx, {
+ type: 'delta',
+ sourceKey: 'lols',
+ added: [],
+ removed: [
+ {
+ apiVersion: '1.0.0',
+ metadata: {
+ name: 'new-root',
+ },
+ kind: 'Location',
+ } as Entity,
+ ],
+ });
+ });
+
+ const currentRefreshState = await db(
+ 'refresh_state',
+ ).select();
+
+ const currentRefRowState = await db(
+ 'refresh_state_references',
+ ).select();
+
+ expect(
+ currentRefreshState.some(
+ t => t.entity_ref === 'location:default/new-root',
+ ),
+ ).toBeFalsy();
+
+ expect(
+ currentRefRowState.some(
+ t =>
+ t.source_key === 'lols' &&
+ t.target_entity_ref === 'location:default/new-root',
+ ),
+ ).toBeFalsy();
+ });
+ });
+
+ describe('updateProcessedEntity', () => {
+ it('should throw if the entity does not exist', async () => {
+ await processingDatabase.transaction(async tx => {
+ await expect(
+ processingDatabase.updateProcessedEntity(tx, {
+ id: '9',
+ processedEntity: {
+ apiVersion: '1.0.0',
+ metadata: {
+ name: 'new-root',
+ },
+ kind: 'Location',
+ } as Entity,
+ deferredEntities: [],
+ relations: [],
+ }),
+ ).rejects.toThrow('Processing state not found for 9');
+ });
+ });
+
+ it('should update a processed entity', async () => {
+ await db('refresh_state').insert({
+ entity_id: '321',
+ entity_ref: 'location:default/new-root',
+ unprocessed_entity: '',
+ errors: '',
+ next_update_at: 'now()',
+ last_discovery_at: 'now()',
+ });
+
+ const deferredEntity = {
+ apiVersion: '1.0.0',
+ metadata: {
+ name: 'deferred',
+ },
+ kind: 'Location',
+ } as Entity;
+
+ const relation: EntityRelationSpec = {
+ source: {
+ kind: 'Component',
+ namespace: 'Default',
+ name: 'foo',
+ },
+ target: {
+ kind: 'Component',
+ namespace: 'Default',
+ name: 'foo',
+ },
+ type: 'url',
+ };
+
+ await processingDatabase.transaction(async tx => {
+ await processingDatabase.updateProcessedEntity(tx, {
+ id: '321',
+ processedEntity: {
+ apiVersion: '1.0.0',
+ metadata: {
+ name: 'new-root',
+ },
+ kind: 'Location',
+ } as Entity,
+ deferredEntities: [deferredEntity],
+ relations: [relation],
+ });
+ });
+
+ const deferredResult = await db('refresh_state')
+ .where({ entity_ref: 'location:default/deferred' })
+ .select();
+ expect(deferredResult.length).toBe(1);
+
+ const [relations] = await db('relations')
+ .where({ originating_entity_id: '321' })
+ .select();
+ expect(relations).toEqual({
+ originating_entity_id: '321',
+ source_entity_ref: 'component:default/foo',
+ type: 'url',
+ target_entity_ref: 'component:default/foo',
+ });
+ });
+ });
+});
diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts
index cf2d4e121c..dc5e6036f5 100644
--- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts
+++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts
@@ -15,6 +15,7 @@
*/
import { ConflictError, NotFoundError } from '@backstage/errors';
+import { stringifyEntityRef, Entity } from '@backstage/catalog-model';
import { Knex } from 'knex';
import { Transaction } from '../../database';
import lodash from 'lodash';
@@ -24,20 +25,23 @@ import {
AddUnprocessedEntitiesOptions,
UpdateProcessedEntityOptions,
GetProcessableEntitiesResult,
+ ReplaceUnprocessedEntitiesOptions,
+ RefreshStateItem,
} from './types';
import type { Logger } from 'winston';
-import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
+
import { v4 as uuid } from 'uuid';
+import { JsonObject } from '@backstage/config';
export type DbRefreshStateRow = {
entity_id: string;
entity_ref: string;
unprocessed_entity: string;
- processed_entity: string;
- cache: string;
+ processed_entity?: string;
+ cache?: string;
next_update_at: string;
last_discovery_at: string; // remove?
- errors: string;
+ errors?: string;
};
export type DbRelationsRow = {
@@ -47,10 +51,10 @@ export type DbRelationsRow = {
type: string;
};
-export type DbRefreshStateReferences = {
- source_special_key?: string;
- source_entity_id?: string;
- target_entity_id: string;
+export type DbRefreshStateReferencesRow = {
+ source_key?: string;
+ source_entity_ref?: string;
+ target_entity_ref: string;
};
// The number of items that are sent per batch to the database layer, when
@@ -86,7 +90,6 @@ export class DefaultProcessingDatabase implements ProcessingDatabase {
errors,
})
.where('entity_id', id);
-
if (refreshResult === 0) {
throw new NotFoundError(`Processing state not found for ${id}`);
}
@@ -94,8 +97,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase {
// Schedule all deferred entities for future processing.
await this.addUnprocessedEntities(tx, {
entities: deferredEntities,
- id,
- type: 'entity',
+ entityRef: stringifyEntityRef(processedEntity),
});
// Update fragments
@@ -128,49 +130,230 @@ export class DefaultProcessingDatabase implements ProcessingDatabase {
);
}
+ private async createDelta(
+ tx: Knex.Transaction,
+ options: ReplaceUnprocessedEntitiesOptions,
+ ): Promise<{ toAdd: Entity[]; toRemove: string[] }> {
+ if (options.type === 'delta') {
+ return {
+ toAdd: options.added,
+ toRemove: options.removed.map(e => stringifyEntityRef(e)),
+ };
+ }
+
+ const oldRefs = await tx(
+ 'refresh_state_references',
+ )
+ .where({ source_key: options.sourceKey })
+ .select('target_entity_ref')
+ .then(rows => rows.map(r => r.target_entity_ref));
+
+ const items = options.items.map(entity => ({
+ entity,
+ ref: stringifyEntityRef(entity),
+ }));
+
+ const oldRefsSet = new Set(oldRefs);
+ const newRefsSet = new Set(items.map(item => item.ref));
+ const toAdd = items.filter(item => !oldRefsSet.has(item.ref));
+ const toRemove = oldRefs.filter(ref => !newRefsSet.has(ref));
+
+ return { toAdd: toAdd.map(({ entity }) => entity), toRemove };
+ }
+
+ async replaceUnprocessedEntities(
+ txOpaque: Transaction,
+ options: ReplaceUnprocessedEntitiesOptions,
+ ): Promise {
+ const tx = txOpaque as Knex.Transaction;
+
+ const { toAdd, toRemove } = await this.createDelta(tx, options);
+
+ if (toRemove.length) {
+ // TODO(freben): Batch split, to not hit variable limits?
+ /*
+ WITH RECURSIVE
+ -- All the nodes that can be reached downwards from our root
+ descendants(root_id, entity_ref) AS (
+ SELECT id, target_entity_ref
+ FROM refresh_state_references
+ WHERE source_key = "R1" AND target_entity_ref = "A"
+ UNION
+ SELECT descendants.root_id, target_entity_ref
+ FROM descendants
+ JOIN refresh_state_references ON source_entity_ref = descendants.entity_ref
+ ),
+ -- All the nodes that can be reached upwards from the descendants
+ ancestors(root_id, via_entity_ref, to_entity_ref) AS (
+ SELECT NULL, entity_ref, entity_ref
+ FROM descendants
+ UNION
+ SELECT
+ CASE WHEN source_key IS NOT NULL THEN id ELSE NULL END,
+ source_entity_ref,
+ ancestors.to_entity_ref
+ FROM ancestors
+ JOIN refresh_state_references ON target_entity_ref = ancestors.via_entity_ref
+ )
+ -- Start out with all of the descendants
+ SELECT descendants.entity_ref
+ FROM descendants
+ -- Expand with all ancestors that point to those, but aren't the current root
+ LEFT OUTER JOIN ancestors
+ ON ancestors.to_entity_ref = descendants.entity_ref
+ AND ancestors.root_id IS NOT NULL
+ AND ancestors.root_id != descendants.root_id
+ -- Exclude all lines that had such a foreign ancestor
+ WHERE ancestors.root_id IS NULL;
+ */
+ const removedCount = await tx('refresh_state')
+ .whereIn('entity_ref', function orphanedEntityRefs(orphans) {
+ return (
+ orphans
+ // All the nodes that can be reached downwards from our root
+ .withRecursive('descendants', function descendants(outer) {
+ return outer
+ .select({ root_id: 'id', entity_ref: 'target_entity_ref' })
+ .from('refresh_state_references')
+ .where('source_key', options.sourceKey)
+ .whereIn('target_entity_ref', toRemove)
+ .union(function recursive(inner) {
+ return inner
+ .select({
+ root_id: 'descendants.root_id',
+ entity_ref:
+ 'refresh_state_references.target_entity_ref',
+ })
+ .from('descendants')
+ .join('refresh_state_references', {
+ 'descendants.entity_ref':
+ 'refresh_state_references.source_entity_ref',
+ });
+ });
+ })
+ // All the nodes that can be reached upwards from the descendants
+ .withRecursive('ancestors', function ancestors(outer) {
+ return outer
+ .select({
+ root_id: tx.raw('NULL', []),
+ via_entity_ref: 'entity_ref',
+ to_entity_ref: 'entity_ref',
+ })
+ .from('descendants')
+ .union(function recursive(inner) {
+ return inner
+ .select({
+ root_id: tx.raw(
+ 'CASE WHEN source_key IS NOT NULL THEN id ELSE NULL END',
+ [],
+ ),
+ via_entity_ref: 'source_entity_ref',
+ to_entity_ref: 'ancestors.to_entity_ref',
+ })
+ .from('ancestors')
+ .join('refresh_state_references', {
+ target_entity_ref: 'ancestors.via_entity_ref',
+ });
+ });
+ })
+ // Start out with all of the descendants
+ .select('descendants.entity_ref')
+ .from('descendants')
+ // Expand with all ancestors that point to those, but aren't the current root
+ .leftOuterJoin('ancestors', function keepaliveRoots() {
+ this.on(
+ 'ancestors.to_entity_ref',
+ '=',
+ 'descendants.entity_ref',
+ );
+ this.andOnNotNull('ancestors.root_id');
+ this.andOn('ancestors.root_id', '!=', 'descendants.root_id');
+ })
+ .whereNull('ancestors.root_id')
+ );
+ })
+ .delete();
+
+ await tx('refresh_state_references')
+ .where('source_key', '=', options.sourceKey)
+ .whereIn('target_entity_ref', toRemove)
+ .delete();
+
+ this.logger.debug(
+ `removed, ${removedCount} entities: ${JSON.stringify(toRemove)}`,
+ );
+ }
+
+ if (toAdd.length) {
+ const state: Knex.DbRecord[] = toAdd.map(entity => ({
+ entity_id: uuid(),
+ entity_ref: stringifyEntityRef(entity),
+ unprocessed_entity: JSON.stringify(entity),
+ errors: '',
+ next_update_at: tx.fn.now(),
+ last_discovery_at: tx.fn.now(),
+ }));
+
+ const stateReferences: DbRefreshStateReferencesRow[] = toAdd.map(
+ entity => ({
+ source_key: options.sourceKey,
+ target_entity_ref: stringifyEntityRef(entity),
+ }),
+ );
+ // TODO(freben): Concurrency? If we did these one by one, a .onConflict().merge would have made sense
+ await tx.batchInsert('refresh_state', state, BATCH_SIZE);
+ await tx.batchInsert(
+ 'refresh_state_references',
+ stateReferences,
+ BATCH_SIZE,
+ );
+ }
+ }
+
async addUnprocessedEntities(
txOpaque: Transaction,
options: AddUnprocessedEntitiesOptions,
): Promise {
const tx = txOpaque as Knex.Transaction;
- const entityIds = new Array();
- for (const entity of options.entities) {
- const entityRef = stringifyEntityRef(entity);
- await tx('refresh_state')
- .insert({
+ const stateRows = options.entities.map(
+ entity =>
+ ({
entity_id: uuid(),
- entity_ref: entityRef,
+ entity_ref: stringifyEntityRef(entity),
unprocessed_entity: JSON.stringify(entity),
errors: '',
next_update_at: tx.fn.now(),
last_discovery_at: tx.fn.now(),
- })
+ } as Knex.DbRecord),
+ );
+ const stateReferenceRows = stateRows.map(
+ stateRow =>
+ ({
+ source_entity_ref: options.entityRef,
+ target_entity_ref: stateRow.entity_ref,
+ } as Knex.DbRecord),
+ );
+
+ // Upsert all of the unprocessed entities into the refresh_state table, by
+ // their entity ref.
+ // TODO(freben): Can this be batched somehow?
+ for (const row of stateRows) {
+ await tx