Merge branch 'master' into multiple-filter-params

This commit is contained in:
Will Sewell
2021-04-29 16:09:33 +01:00
188 changed files with 4896 additions and 925 deletions
-21
View File
@@ -1,21 +0,0 @@
---
'@backstage/plugin-api-docs': patch
'@backstage/plugin-bitrise': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-circleci': patch
'@backstage/plugin-cost-insights': patch
'@backstage/plugin-explore': patch
'@backstage/plugin-fossa': patch
'@backstage/plugin-github-actions': patch
'@backstage/plugin-github-deployments': patch
'@backstage/plugin-graphiql': patch
'@backstage/plugin-jenkins': patch
'@backstage/plugin-lighthouse': patch
'@backstage/plugin-pagerduty': patch
'@backstage/plugin-register-component': patch
'@backstage/plugin-rollbar': patch
'@backstage/plugin-sentry': patch
'@backstage/plugin-sonarqube': patch
---
Updated README to have up-to-date install instructions.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core': patch
---
Allow passing NavLinkProps to SidebarItem component to use in NavLink
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-api-docs': patch
---
Added fields filtering in get API entities to avoid the requesting of unused data
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/create-app': patch
---
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"`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-code-coverage-backend': patch
---
Include migrations
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/plugin-catalog': patch
'@backstage/plugin-scaffolder': patch
---
Removed unused `swr` dependency.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-search-backend-node': patch
---
Introduced Scheduler which is responsible for adding new tasks to a schedule together with it's interval timer as well as starting and stopping the scheduler processes.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/techdocs-common': patch
---
Gracefully handle HTTP request failures in download method of AzureBlobStorage publisher.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-search': patch
---
Use `RouteRef` to generate path to search page.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-tech-radar': patch
---
Update README for composability
@@ -1,6 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Use `EntityRefLink` in header and use relations to reference the owner of the
document.
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Fix TechDocs landing page table wrong copied link
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Add customization and exportable components for TechDocs landing page
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Disable color transitions on links to avoid issues in dark mode.
-12
View File
@@ -1,12 +0,0 @@
---
'@backstage/plugin-techdocs': minor
---
Make `techdocsStorageApiRef` and `techdocsApiRef` use interfaces instead of the
actual implementation classes.
This renames the classes `TechDocsApi` to `TechDocsClient` and `TechDocsStorageApi`
to `TechDocsStorageClient` and renames the interfaces `TechDocs` to `TechDocsApi`
and `TechDocsStorage` to `TechDocsStorageApi` to comply the pattern elsewhere in
the project. This also fixes the types returned by some methods on those
interfaces.
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Rework state management to avoid rendering multiple while navigating between pages.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-proxy-backend': patch
---
Prefix proxy routes with `/` if not present in configuration
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Added the `nebula-preview` preview to `Octokit` for repository visibility.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core': patch
---
Improve owner example value in `MissingAnnotationEmptyState`.
+9
View File
@@ -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`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog': patch
---
Switch out the time-based personal greeting for a plain title on the catalog index page, and remove the clocks for different timezones.
+10
View File
@@ -129,6 +129,16 @@ For the CA, create a `configMap` named `<release name>-<chart name>-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:
@@ -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 -}}
@@ -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 }}
@@ -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 }}
@@ -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 }}
PGPATH_TO_CA: {{ include "backstage.lighthouse.postgresCaFilename" . | quote }}
{{- end }}
@@ -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 }}
@@ -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 }}
@@ -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 }}
+7 -2
View File
@@ -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
@@ -51,9 +51,8 @@ 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/conf.d/default.conf.template
COPY docker/run.sh /usr/local/bin/run.sh
COPY docker/default.conf.template /etc/nginx/templates/default.conf.template
CMD run.sh
COPY docker/inject-config.sh /docker-entrypoint.d/40-inject-config.sh
ENV PORT 80
@@ -33,9 +33,9 @@ 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/conf.d/default.conf.template
COPY docker/run.sh /usr/local/bin/run.sh
COPY docker/default.conf.template /etc/nginx/templates/default.conf.template
CMD run.sh
COPY docker/inject-config.sh /docker-entrypoint.d/40-inject-config.sh
ENV PORT 80
@@ -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;'
@@ -2,15 +2,15 @@
# Variables
#==========================
variable "backstage-bucket" {
variable "backstage_bucket" {
default = "backstage_bucket_for_my_corp"
}
variable "backstage-iam" {
variable "backstage_iam" {
default = "backstage"
}
variable "shared-managed-tag-value" {
variable "shared_managed_tag" {
default = "terraform_for_my_corp"
}
@@ -19,7 +19,7 @@ variable "shared-managed-tag-value" {
#==========================
resource "aws_s3_bucket" "backstage" {
bucket = var.backstage-bucket
bucket = var.backstage_bucket
acl = "private"
provider = aws
@@ -36,8 +36,8 @@ resource "aws_s3_bucket" "backstage" {
}
tags = {
Name = var.backstage-bucket
"Managed By Terraform" = var.shared-managed-tag-value
Name = var.backstage_bucket
"Managed By Terraform" = var.shared_managed_tag
}
}
@@ -56,16 +56,16 @@ resource "aws_s3_bucket_public_access_block" "backstage" {
#==========================
resource "aws_iam_user" "backstage" {
name = var.backstage-iam
name = var.backstage_iam
}
resource "aws_iam_user_policy" "backstage" {
name = var.backstage-iam
name = var.backstage_iam
user = aws_iam_user.backstage.name
policy = data.aws_iam_policy_document.backstage-policy.json
policy = data.aws_iam_policy_document.backstage_policy.json
}
data "aws_iam_policy_document" "backstage-policy" {
data "aws_iam_policy_document" "backstage_policy" {
statement {
actions = [
"s3:PutObject",
+76
View File
@@ -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.
*/
/// <reference types="cypress" />
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');
});
});
});
@@ -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"
}
}
```
+6 -5
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

+42
View File
@@ -1,5 +1,47 @@
# 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
+18 -18
View File
@@ -1,39 +1,39 @@
{
"name": "example-app",
"version": "0.2.25",
"version": "0.2.26",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.7.7",
"@backstage/cli": "^0.6.9",
"@backstage/core": "^0.7.6",
"@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.5",
"@backstage/plugin-catalog-import": "^0.5.3",
"@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-code-coverage": "^0.1.2",
"@backstage/plugin-cost-insights": "^0.8.4",
"@backstage/plugin-explore": "^0.3.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.1",
"@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-tech-radar": "^0.3.9",
"@backstage/plugin-techdocs": "^0.8.0",
"@backstage/plugin-techdocs": "^0.9.0",
"@backstage/plugin-todo": "^0.1.0",
"@backstage/plugin-user-settings": "^0.2.8",
"@backstage/theme": "^0.2.6",
+1 -1
View File
@@ -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"]
+1 -1
View File
@@ -32,7 +32,7 @@ export default async function createPlugin({
indexBuilder.addCollator({
type: 'software-catalog',
defaultRefreshIntervalSeconds: 600,
collator: new DefaultCatalogCollator(discovery),
collator: new DefaultCatalogCollator({ discovery }),
});
const { scheduler } = await indexBuilder.build();
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+8
View File
@@ -1,5 +1,13 @@
# @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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.7.6",
"version": "0.7.7",
"private": false,
"publishConfig": {
"access": "public",
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export { TabbedLayout } from './TabbedLayout';
export { RoutedTabs } from './RoutedTabs';
+39
View File
@@ -1,5 +1,44 @@
# @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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.3.19",
"version": "0.3.20",
"private": false,
"publishConfig": {
"access": "public"
+6
View File
@@ -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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/techdocs-common",
"description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli",
"version": "0.5.0",
"version": "0.5.1",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -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 !`,
});
});
});
});
@@ -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();
});
@@ -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 !`,
});
});
});
});
@@ -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);
@@ -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 !`,
});
});
});
});
+14
View File
@@ -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
+3 -3
View File
@@ -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,9 +29,9 @@
"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.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/theme": "^0.2.6",
"@material-icons/font": "^1.0.2",
@@ -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,
},
+1 -1
View File
@@ -21,7 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.6",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/errors": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.3",
"@backstage/theme": "^0.2.6",
+10
View File
@@ -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
+2 -2
View File
@@ -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,7 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.7.2",
"@backstage/core": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/plugin-catalog-react": "^0.1.2",
"@backstage/theme": "^0.2.6",
"@material-ui/core": "^4.11.0",
+8
View File
@@ -1,5 +1,13 @@
# @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
@@ -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');
};
@@ -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');
};
@@ -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;`);
};
@@ -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');
};
@@ -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;`);
};
@@ -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;
`);
};
@@ -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();
});
}
};
@@ -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();
};
@@ -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() {};
@@ -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');
});
};
@@ -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');
});
};
@@ -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');
});
};
@@ -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');
});
};
@@ -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');
};
@@ -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']);
});
}
};
@@ -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');
});
}
};
@@ -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;
`);
}
};
@@ -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');
});
}
};
@@ -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');
+3 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
"version": "0.8.0",
"version": "0.8.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,10 +33,9 @@
"@backstage/backend-common": "^0.6.3",
"@backstage/catalog-model": "^0.7.7",
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.6",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@backstage/plugin-search-backend-node": "^0.1.3",
"@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,8 +60,7 @@
"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.9",
@@ -80,7 +78,6 @@
"files": [
"dist",
"migrations/**/*.{js,d.ts}",
"migrationsv2/**/*.{js,d.ts}",
"config.d.ts"
],
"configSchema": "config.d.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',
},
}),
]),
});
});
});
@@ -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<void> {
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,
});
}
}
@@ -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<T>(key: ContextKey<T>): T {
return key.defaultValue;
}
}
@@ -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<unknown>, value: unknown) {
return new ContextWithValue(parent, key, value);
}
private constructor(
private readonly parent: Context,
private readonly key: ContextKey<unknown>,
private readonly value: unknown,
) {}
getContextValue<T>(key: ContextKey<T>): T {
if (this.key === key) {
return this.value as T;
}
return this.parent.getContextValue(key);
}
}
@@ -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<Knex.Transaction | undefined>(
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;
}
}
@@ -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';
@@ -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<T> {
constructor(readonly defaultValue: T) {}
}
export interface Context {
getContextValue<T>(key: ContextKey<T>): T;
}
@@ -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<EntityMessage>
>();
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<EntityMessage> {
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);
};
});
}
}
@@ -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<void> {
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<string>([
@@ -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
}
}
}
@@ -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<string>();
@@ -268,7 +268,7 @@ function createEmitter(logger: Logger, parentEntity: Entity) {
const errors = new Array<Error>();
const relations = new Array<EntityRelationSpec>();
const deferredEntites = new Array<Entity>();
const deferredEntities = new Array<Entity>();
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,
};
},
};
@@ -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',
},
}),
]),
});
});
});
});
@@ -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<LocationMessage>
>();
export class DefaultLocationStore implements LocationStore, EntityProvider {
private _connection: EntityProviderConnection | undefined;
constructor(private readonly db: Database) {}
createLocation(spec: LocationSpec): Promise<Location> {
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<Location> {
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<Location[]> {
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<Location> {
@@ -75,40 +85,38 @@ export class DefaultLocationStore implements LocationStore {
}
deleteLocation(id: string): Promise<void> {
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<LocationMessage> {
return new ObservableImpl<LocationMessage>(subscriber => {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
async connect(connection: EntityProviderConnection): Promise<void> {
this._connection = connection;
const locations = await this.listLocations();
const entities = locations.map(location => {
return locationSpecToLocationEntity(location);
});
await this.connection.applyMutation({
type: 'full',
entities,
});
}
}

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