Merge branch 'master' into multiple-filter-params
This commit is contained in:
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
Allow passing NavLinkProps to SidebarItem component to use in NavLink
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': patch
|
||||
---
|
||||
|
||||
Added fields filtering in get API entities to avoid the requesting of unused data
|
||||
@@ -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"`.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-code-coverage-backend': patch
|
||||
---
|
||||
|
||||
Include migrations
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Removed unused `swr` dependency.
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
Gracefully handle HTTP request failures in download method of AzureBlobStorage publisher.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Use `RouteRef` to generate path to search page.
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Add customization and exportable components for TechDocs landing page
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Disable color transitions on links to avoid issues in dark mode.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-proxy-backend': patch
|
||||
---
|
||||
|
||||
Prefix proxy routes with `/` if not present in configuration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Added the `nebula-preview` preview to `Octokit` for repository visibility.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
Improve owner example value in `MissingAnnotationEmptyState`.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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 }}
|
||||
+2
-15
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
|
||||
-8
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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,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 !`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
-9
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
+45
@@ -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');
|
||||
};
|
||||
+44
@@ -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;`);
|
||||
};
|
||||
+87
@@ -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');
|
||||
});
|
||||
};
|
||||
+50
@@ -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');
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user