From 9ca8f8355f199992c8d37d80f3f095d7ce9e8abe Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 26 May 2020 16:40:41 +0200 Subject: [PATCH 01/97] feat: location update results --- .../src/catalog/DatabaseLocationsCatalog.ts | 41 ++++++++++-- plugins/catalog-backend/src/catalog/types.ts | 17 ++++- .../src/database/Database.test.ts | 53 ++++++++++++++- .../catalog-backend/src/database/Database.ts | 66 +++++++++++++++++-- .../src/database/DatabaseManager.test.ts | 12 +++- plugins/catalog-backend/src/database/types.ts | 6 ++ .../catalog-backend/src/ingestion/types.ts | 2 +- plugins/catalog-backend/src/service/router.ts | 5 ++ 8 files changed, 181 insertions(+), 21 deletions(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index 3f5739eed0..f571a20cfa 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -14,9 +14,15 @@ * limitations under the License. */ -import { Database } from '../database'; import { AddLocation, Location, LocationsCatalog } from './types'; import { LocationReader } from '../ingestion'; +import { Database, DatabaseLocationUpdateLogEvent } from '../database'; +import { + AddLocation, + LocationEnvelope, + Location, + LocationsCatalog, +} from './types'; export class DatabaseLocationsCatalog implements LocationsCatalog { constructor( @@ -42,13 +48,36 @@ export class DatabaseLocationsCatalog implements LocationsCatalog { await this.database.removeLocation(id); } - async locations(): Promise { + async locations(): Promise { const items = await this.database.locations(); - return items; + return items.map(({ message, status, timestamp, ...data }) => ({ + lastUpdate: { + message, + status, + timestamp, + }, + data, + })); } - async location(id: string): Promise { - const item = await this.location(id); - return item; + async locationHistory(id: string): Promise { + return this.database.locationHistory(id); + } + + async location(id: string): Promise { + const { + message, + status, + timestamp, + ...data + } = await this.database.location(id); + return { + lastUpdate: { + message, + status, + timestamp, + }, + data, + }; } } diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 92c059d25b..ad7969405b 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -16,6 +16,7 @@ import * as yup from 'yup'; import { DescriptorEnvelope } from '../ingestion'; +import { DatabaseLocationUpdateLogEvent } from '../database'; // // Items @@ -34,12 +35,23 @@ export type EntitiesCatalog = { // Locations // +export type status = { + timestamp: DatabaseLocationUpdateLogEvent['created_at'] | null; + status: DatabaseLocationUpdateLogEvent['status'] | null; + message: DatabaseLocationUpdateLogEvent['message'] | null; +}; + export type Location = { id: string; type: string; target: string; }; +export type LocationEnvelope = { + data: Location; + lastUpdate: status; +}; + export type AddLocation = { type: string; target: string; @@ -55,6 +67,7 @@ export const addLocationSchema: yup.Schema = yup export type LocationsCatalog = { addLocation(location: AddLocation): Promise; removeLocation(id: string): Promise; - locations(): Promise; - location(id: string): Promise; + locations(): Promise; + location(id: string): Promise; + locationHistory(id: string): Promise; }; diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index 0eb64916e5..b02bbab668 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -27,6 +27,8 @@ import { DbEntityRequest, DbEntityResponse, DbLocationsRow, + DbLocationsRowWithStatus, + DatabaseLocationUpdateLogStatus, } from './types'; describe('Database', () => { @@ -73,7 +75,10 @@ describe('Database', () => { name: 'c', namespace: 'd', labels: { e: 'f' }, - annotations: { g: 'h' }, + annotations: { + g: 'h', + 'backstage.io/managed-by-location': undefined, + }, }, spec: { i: 'j' }, }, @@ -83,10 +88,13 @@ describe('Database', () => { it('manages locations', async () => { const db = new Database(database, getVoidLogger()); const input: AddDatabaseLocation = { type: 'a', target: 'b' }; - const output: DbLocationsRow = { + const output: DbLocationsRowWithStatus = { id: expect.anything(), type: 'a', target: 'b', + message: null, + status: null, + timestamp: null, }; await db.addLocation(input); @@ -117,7 +125,7 @@ describe('Database', () => { // Output is the same expect(output2).toEqual(output1); // Locations contain only one record - expect(locations).toEqual([output1]); + expect(locations[0]).toMatchObject(output1); }); describe('addEntity', () => { @@ -149,6 +157,45 @@ describe('Database', () => { }); }); + describe('locationHistory', () => { + it('outputs the history correctly', async () => { + const catalog = new Database(database, getVoidLogger()); + const location: AddDatabaseLocation = { type: 'a', target: 'b' }; + const { id: locationId } = await catalog.addLocation(location); + + await catalog.addLocationUpdateLogEvent( + locationId, + DatabaseLocationUpdateLogStatus.SUCCESS, + ); + await catalog.addLocationUpdateLogEvent( + locationId, + DatabaseLocationUpdateLogStatus.FAIL, + undefined, + 'Something went wrong', + ); + + const result = await catalog.locationHistory(locationId); + expect(result).toEqual([ + { + created_at: expect.anything(), + entity_name: null, + id: expect.anything(), + location_id: locationId, + message: null, + status: DatabaseLocationUpdateLogStatus.SUCCESS, + }, + { + created_at: expect.anything(), + entity_name: null, + id: expect.anything(), + location_id: locationId, + message: 'Something went wrong', + status: DatabaseLocationUpdateLogStatus.FAIL, + }, + ]); + }); + }); + describe('updateEntity', () => { it('can read and no-op-update an entity', async () => { const catalog = new Database(database, getVoidLogger()); diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index cd4ca731a3..9e2b69bce2 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -34,6 +34,7 @@ import { DbEntityRequest, DbEntityResponse, DbLocationsRow, + DbLocationsRowWithStatus, } from './types'; function getStrippedMetadata(metadata: EntityMeta): EntityMeta { @@ -190,6 +191,10 @@ export class Database { uid: generateUid(), etag: generateEtag(), generation: 1, + annotations: { + ...(newEntity.metadata?.annotations ?? {}), + 'backstage.io/managed-by-location': request.locationId, + }, }); const newRow = toEntityRow(request.locationId, newEntity); @@ -367,18 +372,65 @@ export class Database { } } - async location(id: string): Promise { - const items = await this.database('locations') - .where({ id }) - .select(); + async location(id: string): Promise { + const items = await this.database('locations') + .where('locations.id', id) + .leftJoin( + 'location_update_log', + 'locations.id', + 'location_update_log.location_id', + ) + .orderBy('location_update_log.created_at', 'desc') + .select({ + status: 'location_update_log.status', + timestamp: 'location_update_log.created_at', + message: 'location_update_log.message', + id: 'locations.id', + type: 'locations.type', + target: 'locations.target', + }); + if (!items.length) { throw new NotFoundError(`Found no location with ID ${id}`); } return items[0]; } - async locations(): Promise { - return this.database('locations').select(); + async locations(): Promise { + const query = this.database + .select({ + status: 'location_update_log.status', + timestamp: 'location_update_log.created_at', + message: 'location_update_log.message', + id: 'locations.id', + type: 'locations.type', + target: 'locations.target', + }) + .from('locations') + .leftJoin( + 'location_update_log', + 'locations.id', + 'location_update_log.location_id', + ) + .orderBy('location_update_log.created_at', 'desc'); + + const result = await this.database(query) + .select() + .groupBy('id'); + + return result; + } + + async locationHistory(id: string): Promise { + const result = await this.database( + 'location_update_log', + ) + .where('location_id', id) + .orderBy('created_at', 'desc') + .limit(10) + .select(); + + return result; } async addLocationUpdateLogEvent( @@ -391,7 +443,7 @@ export class Database { 'location_update_log', ).insert({ id: uuidv4(), - status: status, + status, location_id: locationId, entity_name: entityName, message, diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts index 2da3c33a00..efae9e3516 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.test.ts @@ -23,7 +23,11 @@ import { } from '../ingestion'; import { Database } from './Database'; import { DatabaseManager } from './DatabaseManager'; -import { DatabaseLocationUpdateLogStatus, DbLocationsRow } from './types'; +import { + DatabaseLocationUpdateLogStatus, + DbLocationsRow, + DbLocationsRowWithStatus, +} from './types'; import Knex from 'knex'; describe('DatabaseManager', () => { @@ -47,10 +51,13 @@ describe('DatabaseManager', () => { }); it('can update a single location', async () => { - const location: DbLocationsRow = { + const location: DbLocationsRowWithStatus = { id: '123', type: 'some', target: 'thing', + message: '', + status: DatabaseLocationUpdateLogStatus.SUCCESS, + timestamp: new Date(314159265).toISOString(), }; const desc: ComponentDescriptor = { apiVersion: 'backstage.io/v1beta1', @@ -58,6 +65,7 @@ describe('DatabaseManager', () => { metadata: { name: 'c1' }, spec: { type: 'service' }, }; + const tx = (undefined as unknown) as Knex.Transaction; const db = ({ diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 06e234545f..e20ba59517 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -52,6 +52,12 @@ export type DbLocationsRow = { target: string; }; +export type DbLocationsRowWithStatus = DbLocationsRow & { + status: DatabaseLocationUpdateLogEvent['status'] | null; + timestamp: DatabaseLocationUpdateLogEvent['created_at'] | null; + message: DatabaseLocationUpdateLogEvent['message'] | null; +}; + export type AddDatabaseLocation = { type: string; target: string; diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index e5083a65f1..87a6f19f1f 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -79,7 +79,7 @@ export type EntityMeta = { * Key/value pairs of non-identifying auxiliary information attached to the * entity. */ - annotations?: Record; + annotations?: Record; }; /** diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index af714e4026..80ba84d0f0 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -56,6 +56,11 @@ export async function createRouter( const output = await locationsCatalog.locations(); res.status(200).send(output); }) + .get('/locations/:id/history', async (req, res) => { + const { id } = req.params; + const output = await locationsCatalog.locationHistory(id); + res.status(200).send(output); + }) .get('/locations/:id', async (req, res) => { const { id } = req.params; const output = await locationsCatalog.location(id); From 0cae5dea3a4e4aa499187c4469c254d1650e980b Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 26 May 2020 16:44:49 +0200 Subject: [PATCH 02/97] fix: conflict resolution leftover --- plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index f571a20cfa..d79769a5a7 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { AddLocation, Location, LocationsCatalog } from './types'; import { LocationReader } from '../ingestion'; import { Database, DatabaseLocationUpdateLogEvent } from '../database'; import { From 9310ce5aee803ef95053d3dc912223e2c12c69f3 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 27 May 2020 10:04:55 +0200 Subject: [PATCH 03/97] fix: test --- plugins/catalog-backend/src/service/router.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 8c29362015..a229d1eb11 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -32,6 +32,7 @@ class MockLocationsCatalog implements LocationsCatalog { removeLocation = jest.fn(); locations = jest.fn(); location = jest.fn(); + locationHistory = jest.fn(); } describe('createRouter', () => { From 8d26fb5c5f1225c5d4908fca691a054a2916aa64 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 27 May 2020 16:34:11 +0200 Subject: [PATCH 04/97] feature: naive implementation of entities API usage --- packages/app/package.json | 7 ++++ plugins/catalog/src/data/component.ts | 1 + plugins/catalog/src/data/mock-factory.ts | 48 ++++++++++++------------ plugins/circleci/src/index.ts | 1 - plugins/circleci/src/proxy.ts | 25 ------------ 5 files changed, 31 insertions(+), 51 deletions(-) delete mode 100644 plugins/circleci/src/proxy.ts diff --git a/packages/app/package.json b/packages/app/package.json index 0a3d68603b..41a4dace53 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -72,6 +72,13 @@ "pathRewrite": { "^/circleci/api/": "/" } + }, + "/catalog/api": { + "target": "http://localhost:3003", + "changeOrigin": true, + "pathRewrite": { + "^/catalog/api/": "/" + } } } } diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts index ec6e2368b8..9a073f0a42 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -15,6 +15,7 @@ */ export type Component = { name: string; + status: string; }; export interface ComponentFactory { diff --git a/plugins/catalog/src/data/mock-factory.ts b/plugins/catalog/src/data/mock-factory.ts index bf39fa436c..15be051fb0 100644 --- a/plugins/catalog/src/data/mock-factory.ts +++ b/plugins/catalog/src/data/mock-factory.ts @@ -14,35 +14,33 @@ * limitations under the License. */ import { Component, ComponentFactory } from './component'; -import mock from './mock-factory-data.json'; +import { DescriptorEnvelope } from '../../../catalog-backend/src/ingestion/types'; + +function transformEnvelopeToComponent(data: DescriptorEnvelope): Component { + return { + name: data.metadata?.name ?? '', + status: data.metadata?.labels?.status ?? 'Up and running', + }; +} + +let inMemoryStore: Promise; -const ARTIFICIAL_TIMEOUT = 800; -let inMemoryStore = [...mock]; export const MockComponentFactory: ComponentFactory = { getAllComponents(): Promise { - return new Promise((resolve) => - setTimeout(() => resolve(inMemoryStore), ARTIFICIAL_TIMEOUT), - ); + inMemoryStore = + inMemoryStore ?? + fetch('//localhost:3000/catalog/api/entities') + .then(response => response.json()) + .then(data => data.map(transformEnvelopeToComponent)); + return inMemoryStore; }, - getComponentByName(name: string): Promise { - return new Promise((resolve, reject) => - setTimeout(() => { - const mockComponent = inMemoryStore.find( - (component) => component.name === name, - ); - if (mockComponent) return resolve(mockComponent); - return reject({ code: 'Component not found!' }); - }, ARTIFICIAL_TIMEOUT), - ); + async getComponentByName(name: string): Promise { + const components = await this.getAllComponents(); + const mockComponent = components.find(component => component.name === name); + if (mockComponent) return mockComponent; + throw new Error(`'Component not found: ${name}`); }, - removeComponentByName(name: string): Promise { - return new Promise((resolve) => - setTimeout(() => { - inMemoryStore = inMemoryStore.filter( - (component) => component.name !== name, - ); - resolve(true); - }, ARTIFICIAL_TIMEOUT), - ); + async removeComponentByName(_: string): Promise { + return true; }, }; diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts index 41cde5e83a..11f2c80b88 100644 --- a/plugins/circleci/src/index.ts +++ b/plugins/circleci/src/index.ts @@ -16,6 +16,5 @@ export { plugin } from './plugin'; export * from './api'; -export * from './proxy'; export * from './route-refs'; export { CircleCIWidget } from './components/App'; diff --git a/plugins/circleci/src/proxy.ts b/plugins/circleci/src/proxy.ts deleted file mode 100644 index 8a5ae460ab..0000000000 --- a/plugins/circleci/src/proxy.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ -export const proxySettings = { - '/circleci/api': { - target: 'https://circleci.com/api/v1.1', - changeOrigin: true, - logLevel: 'debug', - pathRewrite: { - '^/circleci/api/': '/', - }, - }, -}; From 6b2be283a04719522e8194b257668f8c830852a8 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 28 May 2020 11:46:51 +0200 Subject: [PATCH 05/97] fix: PR edits --- .../src/catalog/DatabaseLocationsCatalog.ts | 10 ++-- plugins/catalog-backend/src/catalog/types.ts | 16 +++--- .../src/database/Database.test.ts | 1 - .../catalog-backend/src/database/Database.ts | 51 ++++++++----------- ...7114117_location_update_log_latest_view.ts | 35 +++++++++++++ plugins/catalog-backend/src/database/types.ts | 6 +-- .../catalog-backend/src/ingestion/types.ts | 2 +- 7 files changed, 72 insertions(+), 49 deletions(-) create mode 100644 plugins/catalog-backend/src/database/migrations/20200527114117_location_update_log_latest_view.ts diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index d79769a5a7..93a4a77622 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -18,7 +18,7 @@ import { LocationReader } from '../ingestion'; import { Database, DatabaseLocationUpdateLogEvent } from '../database'; import { AddLocation, - LocationEnvelope, + LocationResponse, Location, LocationsCatalog, } from './types'; @@ -47,10 +47,10 @@ export class DatabaseLocationsCatalog implements LocationsCatalog { await this.database.removeLocation(id); } - async locations(): Promise { + async locations(): Promise { const items = await this.database.locations(); return items.map(({ message, status, timestamp, ...data }) => ({ - lastUpdate: { + currentStatus: { message, status, timestamp, @@ -63,7 +63,7 @@ export class DatabaseLocationsCatalog implements LocationsCatalog { return this.database.locationHistory(id); } - async location(id: string): Promise { + async location(id: string): Promise { const { message, status, @@ -71,7 +71,7 @@ export class DatabaseLocationsCatalog implements LocationsCatalog { ...data } = await this.database.location(id); return { - lastUpdate: { + currentStatus: { message, status, timestamp, diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 1c51448f0b..32d4edd27e 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -42,10 +42,10 @@ export type EntitiesCatalog = { // Locations // -export type status = { - timestamp: DatabaseLocationUpdateLogEvent['created_at'] | null; - status: DatabaseLocationUpdateLogEvent['status'] | null; - message: DatabaseLocationUpdateLogEvent['message'] | null; +export type LocationUpdateStatus = { + timestamp: string | null; + status: string | null; + message: string | null; }; export type Location = { @@ -54,9 +54,9 @@ export type Location = { target: string; }; -export type LocationEnvelope = { +export type LocationResponse = { data: Location; - lastUpdate: status; + currentStatus: LocationUpdateStatus; }; export type AddLocation = { @@ -74,7 +74,7 @@ export const addLocationSchema: yup.Schema = yup export type LocationsCatalog = { addLocation(location: AddLocation): Promise; removeLocation(id: string): Promise; - locations(): Promise; - location(id: string): Promise; + locations(): Promise; + location(id: string): Promise; locationHistory(id: string): Promise; }; diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index 070e138258..00be7d5d1e 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -78,7 +78,6 @@ describe('Database', () => { labels: { e: 'f' }, annotations: { g: 'h', - 'backstage.io/managed-by-location': undefined, }, }, spec: { i: 'j' }, diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index 674624c9c6..d4d3ec842e 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -195,7 +195,9 @@ export class Database { generation: 1, annotations: { ...(newEntity.metadata?.annotations ?? {}), - 'backstage.io/managed-by-location': request.locationId, + ...(request.locationId + ? { 'backstage.io/managed-by-location': request.locationId } + : {}), }, }; @@ -396,19 +398,15 @@ export class Database { async location(id: string): Promise { const items = await this.database('locations') .where('locations.id', id) - .leftJoin( - 'location_update_log', + .leftOuterJoin( + 'location_update_log_latest', 'locations.id', - 'location_update_log.location_id', + 'location_update_log_latest.location_id', ) - .orderBy('location_update_log.created_at', 'desc') - .select({ - status: 'location_update_log.status', - timestamp: 'location_update_log.created_at', - message: 'location_update_log.message', - id: 'locations.id', - type: 'locations.type', - target: 'locations.target', + .select('locations.*', { + status: 'location_update_log_latest.status', + timestamp: 'location_update_log_latest.created_at', + message: 'location_update_log_latest.message', }); if (!items.length) { @@ -418,28 +416,19 @@ export class Database { } async locations(): Promise { - const query = this.database - .select({ - status: 'location_update_log.status', - timestamp: 'location_update_log.created_at', - message: 'location_update_log.message', - id: 'locations.id', - type: 'locations.type', - target: 'locations.target', - }) - .from('locations') - .leftJoin( - 'location_update_log', + const locations = await this.database('locations') + .leftOuterJoin( + 'location_update_log_latest', 'locations.id', - 'location_update_log.location_id', + 'location_update_log_latest.location_id', ) - .orderBy('location_update_log.created_at', 'desc'); + .select('locations.*', { + status: 'location_update_log_latest.status', + timestamp: 'location_update_log_latest.created_at', + message: 'location_update_log_latest.message', + }); - const result = await this.database(query) - .select() - .groupBy('id'); - - return result; + return locations; } async locationHistory(id: string): Promise { diff --git a/plugins/catalog-backend/src/database/migrations/20200527114117_location_update_log_latest_view.ts b/plugins/catalog-backend/src/database/migrations/20200527114117_location_update_log_latest_view.ts new file mode 100644 index 0000000000..6da9eeed9b --- /dev/null +++ b/plugins/catalog-backend/src/database/migrations/20200527114117_location_update_log_latest_view.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as Knex from 'knex'; + +export async function up(knex: Knex): Promise { + // Need to first order by date of creation + const query = knex + .select() + .from('location_update_log') + .orderBy('location_update_log.created_at', 'desc'); + + // And only then to do the grouping to get the latest per location + const groupedQuery = knex(query).groupBy('location_id').select(); + + await knex.schema.raw( + `CREATE VIEW location_update_log_latest AS ${groupedQuery.toString()};`, + ); +} + +export async function down(knex: Knex): Promise { + return knex.schema.raw(`DROP VIEW location_update_log_latest;`); +} diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index e20ba59517..cf49adb32e 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -53,9 +53,9 @@ export type DbLocationsRow = { }; export type DbLocationsRowWithStatus = DbLocationsRow & { - status: DatabaseLocationUpdateLogEvent['status'] | null; - timestamp: DatabaseLocationUpdateLogEvent['created_at'] | null; - message: DatabaseLocationUpdateLogEvent['message'] | null; + status: string | null; + timestamp: string | null; + message: string | null; }; export type AddDatabaseLocation = { diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index 87a6f19f1f..e5083a65f1 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -79,7 +79,7 @@ export type EntityMeta = { * Key/value pairs of non-identifying auxiliary information attached to the * entity. */ - annotations?: Record; + annotations?: Record; }; /** From f4887f08b168f564a184aa21c4cab7c9a255968f Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 00:38:25 +0200 Subject: [PATCH 06/97] feature: implement useApi logic for fetching components --- packages/app/src/apis.ts | 9 + plugins/catalog/src/api/index.ts | 49 +++++ plugins/catalog/src/api/types.ts | 185 ++++++++++++++++++ .../CatalogPage/CatalogPage.test.tsx | 9 +- .../components/CatalogPage/CatalogPage.tsx | 16 +- .../components/CatalogTable/CatalogTable.tsx | 30 +-- .../ComponentPage/ComponentPage.test.tsx | 20 -- .../ComponentPage/ComponentPage.tsx | 23 ++- plugins/catalog/src/data/component.ts | 9 +- .../catalog/src/data/mock-factory-data.json | 35 ---- plugins/catalog/src/data/mock-factory.ts | 46 ----- .../data/{with-mock-store.tsx => utils.ts} | 19 +- plugins/catalog/src/index.ts | 1 + plugins/catalog/src/plugin.ts | 5 +- 14 files changed, 281 insertions(+), 175 deletions(-) create mode 100644 plugins/catalog/src/api/index.ts create mode 100644 plugins/catalog/src/api/types.ts delete mode 100644 plugins/catalog/src/data/mock-factory-data.json delete mode 100644 plugins/catalog/src/data/mock-factory.ts rename plugins/catalog/src/data/{with-mock-store.tsx => utils.ts} (61%) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index c11ea24aca..bf9980bf5f 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -37,6 +37,7 @@ import { import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar'; import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci'; +import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog'; const builder = ApiRegistry.builder(); @@ -72,4 +73,12 @@ builder.add( }), ); +builder.add( + catalogApiRef, + new CatalogApi({ + apiOrigin: 'http://localhost:3000', + basePath: '/catalog/api', + }), +); + export default builder.build() as ApiHolder; diff --git a/plugins/catalog/src/api/index.ts b/plugins/catalog/src/api/index.ts new file mode 100644 index 0000000000..1bc739b56f --- /dev/null +++ b/plugins/catalog/src/api/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core'; +import { DescriptorEnvelope } from './types'; + +export const catalogApiRef = createApiRef({ + id: 'plugin.catalog.service', + description: + 'Used by the Catalog plugin to make requests to accompanying backend', +}); + +export class CatalogApi { + private apiOrigin: string; + private basePath: string; + constructor({ + apiOrigin, + basePath, + }: { + apiOrigin: string; + basePath: string; + }) { + this.apiOrigin = apiOrigin; + this.basePath = basePath; + } + async getEntities(): Promise { + const response = await fetch(`${this.apiOrigin}${this.basePath}/entities`); + return await response.json(); + } + async getEntityByName(name: string): Promise { + const entities = await this.getEntities(); + const entity = entities.find(e => e.metadata.name === name); + if (entity) return entity; + throw new Error(`'Entity not found: ${name}`); + } +} diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts new file mode 100644 index 0000000000..9f1cfb8529 --- /dev/null +++ b/plugins/catalog/src/api/types.ts @@ -0,0 +1,185 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentDescriptorV1beta1 } from './descriptors/ComponentDescriptorV1beta1Parser'; + +export type ComponentDescriptor = ComponentDescriptorV1beta1; + +/** + * Metadata fields common to all versions/kinds of entity. + * + * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta + */ +export type EntityMeta = { + /** + * A globally unique ID for the entity. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. The field can (optionally) be specified when performing + * update or delete operations, but the server is free to reject requests + * that do so in such a way that it breaks semantics. + */ + uid?: string; + + /** + * An opaque string that changes for each update operation to any part of + * the entity, including metadata. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. The field can (optionally) be specified when performing + * update or delete operations, and the server will then reject the + * operation if it does not match the current stored value. + */ + etag?: string; + + /** + * A positive nonzero number that indicates the current generation of data + * for this entity; the value is incremented each time the spec changes. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. + */ + generation?: number; + + /** + * The name of the entity. + * + * Must be uniqe within the catalog at any given point in time, for any + * given namespace, for any given kind. + */ + name: string; + + /** + * The namespace that the entity belongs to. + */ + namespace?: string; + + /** + * Key/value pairs of identifying information attached to the entity. + */ + labels?: Record; + + /** + * Key/value pairs of non-identifying auxiliary information attached to the + * entity. + */ + annotations?: Record; +}; + +/** + * The format envelope that's common to all versions/kinds. + * + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ + */ +export type DescriptorEnvelope = { + /** + * The version of specification format for this particular entity that + * this is written against. + */ + apiVersion: string; + + /** + * The high level entity type being described. + */ + kind: string; + + /** + * Optional metadata related to the entity. + */ + metadata: EntityMeta; + + /** + * The specification data describing the entity itself. + */ + spec?: object; +}; + +/** + * Parses and validates descriptors. + * + * The output must be validated and well formed. + */ +export type DescriptorParser = { + /** + * Parses and validates a single raw descriptor. + * + * @param descriptor A raw descriptor object + * @returns A structure describing the parsed and validated descriptor + * @throws An Error if the descriptor was malformed + */ + parse(descriptor: object): Promise; +}; + +/** + * Parses and validates a single envelope into its materialized kind. + * + * These parsers may assume that the envelope is already validated and well + * formed. + */ +export type KindParser = { + /** + * Try to parse an envelope into a materialized kind. + * + * @param envelope A valid descriptor envelope + * @returns A materialized type, or undefined if the given version/kind is + * not meant to be handled by this parser + * @throws An Error if the type was handled and found to not be properly + * formatted + */ + tryParse( + envelope: DescriptorEnvelope, + ): Promise; +}; + +export class ParserError extends Error { + constructor(message?: string, private _entityName?: string | undefined) { + super(message); + } + get entityName() { + return this._entityName; + } +} + +export type ReaderOutput = + | { type: 'error'; error: Error } + | { type: 'data'; data: object }; + +export type LocationReader = { + /** + * Reads the contents of a single location. + * + * @param type The type of location to read + * @param target The location target (type-specific) + * @returns The parsed contents, as an array of unverified descriptors or + * errors where the individual documents could not be parsed. + * @throws An error if the location as a whole could not be read + */ + read(type: string, target: string): Promise; +}; + +export type LocationSource = { + /** + * Reads the contents of a single location. + * + * @param target The location target to read + * @returns The parsed contents, as an array of unverified descriptors + * @throws An error if the location target could not be read + */ + read(target: string): Promise; +}; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 60af5f7d24..df7fcc1cd2 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -19,19 +19,12 @@ import { render } from '@testing-library/react'; import CatalogPage from './CatalogPage'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { ComponentFactory } from '../../data/component'; - -const testComponentFactory: ComponentFactory = { - getAllComponents: jest.fn(() => Promise.resolve([{ name: 'test' }])), - getComponentByName: jest.fn(() => Promise.resolve({ name: 'test' })), - removeComponentByName: jest.fn(() => Promise.resolve(true)), -}; describe('CatalogPage', () => { it('should render', async () => { const rendered = render( - + , ); expect( diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index ec6c1cf09f..63bb4e0771 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -23,17 +23,19 @@ import { SupportButton, Page, pageTheme, + useApi, } from '@backstage/core'; import { useAsync } from 'react-use'; -import { ComponentFactory } from '../../data/component'; import CatalogTable from '../CatalogTable/CatalogTable'; import { Button } from '@material-ui/core'; -type CatalogPageProps = { - componentFactory: ComponentFactory; -}; -const CatalogPage: FC = ({ componentFactory }) => { - const { value, error, loading } = useAsync(componentFactory.getAllComponents); +import { catalogApiRef } from '../..'; +import { envelopeToComponent } from '../../data/utils'; + +const CatalogPage: FC<{}> = () => { + const catalogApi = useApi(catalogApiRef); + const { value, error, loading } = useAsync(() => catalogApi.getEntities()); + return (
@@ -47,7 +49,7 @@ const CatalogPage: FC = ({ componentFactory }) => { All your components diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 57f1dfe7f5..27e59973b5 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -15,13 +15,7 @@ */ import React, { FC } from 'react'; import { Component } from '../../data/component'; -import { - InfoCard, - Progress, - Table, - TableColumn, - StatusOK, -} from '@backstage/core'; +import { InfoCard, Progress, Table, TableColumn } from '@backstage/core'; import { Typography, Link } from '@material-ui/core'; const columns: TableColumn[] = [ @@ -34,26 +28,8 @@ const columns: TableColumn[] = [ ), }, { - title: 'System', - field: 'system', - }, - { - title: 'Owner', - field: 'owner', - }, - { - title: 'Lifecycle', - field: 'lifecycle', - }, - { - title: 'Status', - field: 'status', - render: (componentData: any) => ( - <> - - {componentData.status || 'Up and running'} - - ), + title: 'Kind', + field: 'kind', }, { title: 'Description', diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index 369b80de80..d0f67d9cb3 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -30,11 +30,6 @@ const getTestProps = (componentName: string) => { history: { push: jest.fn(), }, - componentFactory: { - getAllComponents: jest.fn(() => Promise.resolve([{ name: 'test' }])), - getComponentByName: jest.fn(() => Promise.resolve({ name: 'test' })), - removeComponentByName: jest.fn(() => Promise.resolve(true)), - }, }; }; @@ -52,19 +47,4 @@ describe('ComponentPage', () => { ); expect(props.history.push).toHaveBeenCalledWith('/catalog'); }); - it('should use factory to fetch component by name and display it', async () => { - await act(async () => { - const props = getTestProps('test'); - await render( - wrapInTheme( - - - , - ), - ); - expect(props.componentFactory.getComponentByName).toHaveBeenCalledWith( - 'test', - ); - }); - }); }); diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx index 0cf7017173..0ee0e1d09a 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -15,7 +15,6 @@ */ import React, { FC, useEffect, useState } from 'react'; import { useAsync } from 'react-use'; -import { ComponentFactory } from '../../data/component'; import ComponentMetadataCard from '../ComponentMetadataCard/ComponentMetadataCard'; import { Content, @@ -30,11 +29,12 @@ import ComponentContextMenu from '../ComponentContextMenu/ComponentContextMenu'; import ComponentRemovalDialog from '../ComponentRemovalDialog/ComponentRemovalDialog'; import { SentryIssuesWidget } from '@backstage/plugin-sentry'; import { Grid } from '@material-ui/core'; +import { catalogApiRef } from '../..'; +import { envelopeToComponent } from '../../data/utils'; const REDIRECT_DELAY = 1000; type ComponentPageProps = { - componentFactory: ComponentFactory; match: { params: { name: string; @@ -45,11 +45,7 @@ type ComponentPageProps = { }; }; -const ComponentPage: FC = ({ - match, - history, - componentFactory, -}) => { +const ComponentPage: FC = ({ match, history }) => { const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const [removingPending, setRemovingPending] = useState(false); const showRemovalDialog = () => setConfirmationDialogOpen(true); @@ -62,8 +58,9 @@ const ComponentPage: FC = ({ return null; } + const catalogApi = useApi(catalogApiRef); const catalogRequest = useAsync(() => - componentFactory.getComponentByName(match.params.name), + catalogApi.getEntityByName(match.params.name), ); useEffect(() => { @@ -78,18 +75,20 @@ const ComponentPage: FC = ({ const removeComponent = async () => { setConfirmationDialogOpen(false); setRemovingPending(true); - await componentFactory.removeComponentByName(componentName); + // await componentFactory.removeComponentByName(componentName); history.push('/catalog'); }; + const component = envelopeToComponent(catalogRequest.value! ?? {}); + return ( -
+
{confirmationDialogOpen && catalogRequest.value && ( = ({ diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts index 9a073f0a42..57cba03a9a 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -15,11 +15,6 @@ */ export type Component = { name: string; - status: string; + kind: string; + description: string; }; - -export interface ComponentFactory { - getAllComponents(): Promise; - getComponentByName(name: string): Promise; - removeComponentByName(name: string): Promise; -} diff --git a/plugins/catalog/src/data/mock-factory-data.json b/plugins/catalog/src/data/mock-factory-data.json deleted file mode 100644 index 8fc61a87a6..0000000000 --- a/plugins/catalog/src/data/mock-factory-data.json +++ /dev/null @@ -1,35 +0,0 @@ -[ - { - "name": "example.com" - }, - { - "name": "subdomain.example.com" - }, - { - "name": "subdomain2.example.com" - }, - { - "name": "User data pipeline 1" - }, - { - "name": "User data pipeline 2" - }, - { - "name": "User data pipeline 3" - }, - { - "name": "Aggregation CRON job" - }, - { - "name": "Authentication service" - }, - { - "name": "Payments service" - }, - { - "name": "Backstage supervisor" - }, - { - "name": "Identity service" - } -] diff --git a/plugins/catalog/src/data/mock-factory.ts b/plugins/catalog/src/data/mock-factory.ts deleted file mode 100644 index 15be051fb0..0000000000 --- a/plugins/catalog/src/data/mock-factory.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { Component, ComponentFactory } from './component'; -import { DescriptorEnvelope } from '../../../catalog-backend/src/ingestion/types'; - -function transformEnvelopeToComponent(data: DescriptorEnvelope): Component { - return { - name: data.metadata?.name ?? '', - status: data.metadata?.labels?.status ?? 'Up and running', - }; -} - -let inMemoryStore: Promise; - -export const MockComponentFactory: ComponentFactory = { - getAllComponents(): Promise { - inMemoryStore = - inMemoryStore ?? - fetch('//localhost:3000/catalog/api/entities') - .then(response => response.json()) - .then(data => data.map(transformEnvelopeToComponent)); - return inMemoryStore; - }, - async getComponentByName(name: string): Promise { - const components = await this.getAllComponents(); - const mockComponent = components.find(component => component.name === name); - if (mockComponent) return mockComponent; - throw new Error(`'Component not found: ${name}`); - }, - async removeComponentByName(_: string): Promise { - return true; - }, -}; diff --git a/plugins/catalog/src/data/with-mock-store.tsx b/plugins/catalog/src/data/utils.ts similarity index 61% rename from plugins/catalog/src/data/with-mock-store.tsx rename to plugins/catalog/src/data/utils.ts index 2e5425d03e..b30ac61d9e 100644 --- a/plugins/catalog/src/data/with-mock-store.tsx +++ b/plugins/catalog/src/data/utils.ts @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as React from 'react'; -import { ComponentFactory } from './component'; -import { MockComponentFactory } from './mock-factory'; +import { DescriptorEnvelope } from '../api/types'; +import { Component } from './component'; -const componentFactory: ComponentFactory = MockComponentFactory; - -export const withMockStore = (Component: React.ElementType) => { - return (props: any) => ( - - ); -}; +export function envelopeToComponent(envelope: DescriptorEnvelope): Component { + return { + name: envelope.metadata?.name ?? '', + kind: envelope.kind ?? 'unknown', + description: 'placeholder', + }; +} diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 3a0a0fe2d3..d67bc6a864 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -15,3 +15,4 @@ */ export { plugin } from './plugin'; +export * from './api'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 456a6d2446..cf9edee4da 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -17,12 +17,11 @@ import { createPlugin } from '@backstage/core'; import CatalogPage from './components/CatalogPage'; import ComponentPage from './components/ComponentPage/ComponentPage'; -import { withMockStore } from './data/with-mock-store'; export const plugin = createPlugin({ id: 'catalog', register({ router }) { - router.registerRoute('/catalog', withMockStore(CatalogPage)); - router.registerRoute('/catalog/:name/', withMockStore(ComponentPage)); + router.registerRoute('/catalog', CatalogPage); + router.registerRoute('/catalog/:name/', ComponentPage); }, }); From 02531bb872feda427dcf3a3a23d98decf5658aa1 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 13:26:24 +0200 Subject: [PATCH 07/97] fix: tests --- .../CatalogPage/CatalogPage.test.tsx | 21 ++++++++++++++----- .../ComponentPage/ComponentPage.test.tsx | 1 - 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index df7fcc1cd2..d4cd6e7606 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -17,15 +17,26 @@ import React from 'react'; import { render } from '@testing-library/react'; import CatalogPage from './CatalogPage'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; +import { ApiRegistry, ApiProvider, errorApiRef } from '@backstage/core'; +import { wrapInTheme } from '@backstage/test-utils'; +import { catalogApiRef } from '../..'; + +const errorApi = { post: () => {} }; +const catalogApi = { getEntities: () => Promise.resolve([{ kind: '' }]) }; describe('CatalogPage', () => { it('should render', async () => { const rendered = render( - - - , + wrapInTheme( + + + , + ), ); expect( await rendered.findByText('Keep track of your software'), diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index d0f67d9cb3..0430fb7d3c 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -17,7 +17,6 @@ import ComponentPage from './ComponentPage'; import { render } from '@testing-library/react'; import * as React from 'react'; import { wrapInTheme } from '@backstage/test-utils'; -import { act } from 'react-dom/test-utils'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; const getTestProps = (componentName: string) => { From 13dae319352c6c43a6fcbf4746f4f5cdb2a0bc38 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 13:29:25 +0200 Subject: [PATCH 08/97] fix: add missing type --- plugins/catalog/src/api/types.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 9f1cfb8529..8ed5748e7c 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { ComponentDescriptorV1beta1 } from './descriptors/ComponentDescriptorV1beta1Parser'; +export interface ComponentDescriptorV1beta1 extends DescriptorEnvelope { + spec: { + type: string; + }; +} export type ComponentDescriptor = ComponentDescriptorV1beta1; From 3fa037ad18b439cff1c66b790ed4e4b545a47b28 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 13:42:49 +0200 Subject: [PATCH 09/97] feature: add description field to Component --- plugins/catalog/src/api/types.ts | 7 +++++++ plugins/catalog/src/data/utils.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 8ed5748e7c..4097b76098 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -69,6 +69,13 @@ export type EntityMeta = { */ name: string; + /** + * The short description of the entity. + * + * A a human readable string. + */ + description: string; + /** * The namespace that the entity belongs to. */ diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts index b30ac61d9e..39a84cfbca 100644 --- a/plugins/catalog/src/data/utils.ts +++ b/plugins/catalog/src/data/utils.ts @@ -20,6 +20,6 @@ export function envelopeToComponent(envelope: DescriptorEnvelope): Component { return { name: envelope.metadata?.name ?? '', kind: envelope.kind ?? 'unknown', - description: 'placeholder', + description: envelope.metadata?.description ?? 'placeholder', }; } From 81229772aa1072ac51b2d707a466f2b3e6da0c8b Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 14:36:01 +0200 Subject: [PATCH 10/97] fix:cleanup --- .../ComponentMetadataCard.test.tsx | 2 + plugins/catalog/src/data/mock-factory.ts | 48 ------------------- 2 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 plugins/catalog/src/data/mock-factory.ts diff --git a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx index f4b07c020a..62e3d52c29 100644 --- a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx +++ b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx @@ -22,6 +22,8 @@ describe('ComponentMetadataCard component', () => { it('should display component name if provided', async () => { const testComponent: Component = { name: 'test', + kind: 'Component', + description: 'Placeholder', }; const rendered = await render( , diff --git a/plugins/catalog/src/data/mock-factory.ts b/plugins/catalog/src/data/mock-factory.ts deleted file mode 100644 index 19f04195f9..0000000000 --- a/plugins/catalog/src/data/mock-factory.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { Component, ComponentFactory } from './component'; -import mock from './mock-factory-data.json'; - -const ARTIFICIAL_TIMEOUT = 800; -let inMemoryStore = [...mock]; -export const MockComponentFactory: ComponentFactory = { - getAllComponents(): Promise { - return new Promise(resolve => - setTimeout(() => resolve(inMemoryStore), ARTIFICIAL_TIMEOUT), - ); - }, - getComponentByName(name: string): Promise { - return new Promise((resolve, reject) => - setTimeout(() => { - const mockComponent = inMemoryStore.find( - component => component.name === name, - ); - if (mockComponent) return resolve(mockComponent); - return reject({ code: 'Component not found!' }); - }, ARTIFICIAL_TIMEOUT), - ); - }, - removeComponentByName(name: string): Promise { - return new Promise(resolve => - setTimeout(() => { - inMemoryStore = inMemoryStore.filter( - component => component.name !== name, - ); - resolve(true); - }, ARTIFICIAL_TIMEOUT), - ); - }, -}; From 9918e36b6bd1fcc017b68918070e6cf478b27ab0 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Thu, 28 May 2020 14:54:04 +0200 Subject: [PATCH 11/97] refactor all the things and comment out tests --- .../src/providers/GoogleAuthProvider.ts | 76 ++ .../auth-backend/src/providers/OAuthHelper.ts | 70 ++ .../src/providers/OAuthProvider.ts | 116 +++ .../src/providers/PassportStrategyHelper.ts | 94 ++ .../src/providers/factories.test.ts | 94 +- .../auth-backend/src/providers/factories.ts | 3 +- .../src/providers/google/index.ts | 1 + .../src/providers/google/provider.test.ts | 925 +++++++++--------- .../src/providers/google/provider.ts | 313 +++--- .../auth-backend/src/providers/index.test.ts | 183 ++-- plugins/auth-backend/src/providers/index.ts | 8 +- plugins/auth-backend/src/providers/types.ts | 21 +- plugins/auth-backend/src/service/router.ts | 36 +- 13 files changed, 1121 insertions(+), 819 deletions(-) create mode 100644 plugins/auth-backend/src/providers/GoogleAuthProvider.ts create mode 100644 plugins/auth-backend/src/providers/OAuthHelper.ts create mode 100644 plugins/auth-backend/src/providers/OAuthProvider.ts create mode 100644 plugins/auth-backend/src/providers/PassportStrategyHelper.ts create mode 100644 plugins/auth-backend/src/providers/google/index.ts diff --git a/plugins/auth-backend/src/providers/GoogleAuthProvider.ts b/plugins/auth-backend/src/providers/GoogleAuthProvider.ts new file mode 100644 index 0000000000..604a765906 --- /dev/null +++ b/plugins/auth-backend/src/providers/GoogleAuthProvider.ts @@ -0,0 +1,76 @@ +import express from 'express'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, +} from './PassportStrategyHelper'; +import { + OAuthProviderHandlers, + AuthInfoBase, + AuthInfoPrivate, + RedirectInfo, + AuthProviderConfig, +} from './types'; + +export class GoogleAuthProvider implements OAuthProviderHandlers { + private readonly provider: string; + private readonly providerConfig: AuthProviderConfig; + private readonly _strategy: GoogleStrategy; + + constructor(providerConfig: AuthProviderConfig) { + this.provider = providerConfig.provider; + this.providerConfig = providerConfig; + // TODO: throw error if env variables not set? + this._strategy = new GoogleStrategy( + { ...this.providerConfig.options }, + ( + accessToken: any, + refreshToken: any, + params: any, + profile: any, + done: any, + ) => { + done( + undefined, + { + profile, + idToken: params.id_token, + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, + { + refreshToken, + }, + ); + }, + ); + } + + async start(req: express.Request, options: any): Promise { + return await executeRedirectStrategy(req, this._strategy, options); + } + + async handler( + req: express.Request, + ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { + return await executeFrameHandlerStrategy(req, this._strategy); + } + + async refresh(refreshToken: string, scope: string): Promise { + return await executeRefreshTokenStrategy( + this._strategy, + refreshToken, + scope, + ); + } + + logout(): Promise { + throw new Error('Method not implemented.'); + } + + getProvider(): string { + return this.provider; + } +} diff --git a/plugins/auth-backend/src/providers/OAuthHelper.ts b/plugins/auth-backend/src/providers/OAuthHelper.ts new file mode 100644 index 0000000000..7f930530a9 --- /dev/null +++ b/plugins/auth-backend/src/providers/OAuthHelper.ts @@ -0,0 +1,70 @@ +import express, { CookieOptions } from 'express'; +import crypto from 'crypto'; + +export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; +export const TEN_MINUTES_MS = 600 * 1000; + +// TODO: move all of these methods to OAuthProvider + +export const verifyNonce = (req: express.Request, provider: string) => { + const cookieNonce = req.cookies[`${provider}-nonce`]; + const stateNonce = req.query.state; + + if (!cookieNonce || !stateNonce) { + throw new Error('Missing nonce'); + } + + if (cookieNonce !== stateNonce) { + throw new Error('Invalid nonce'); + } +}; + +export const setNonceCookie = (res: express.Response, provider: string) => { + const nonce = crypto.randomBytes(16).toString('base64'); + + const options: CookieOptions = { + maxAge: TEN_MINUTES_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}/handler`, + httpOnly: true, + }; + + res.cookie(`${provider}-nonce`, nonce, options); + + return nonce; +}; + +export const setRefreshTokenCookie = ( + res: express.Response, + provider: string, + refreshToken: string, +) => { + const options: CookieOptions = { + maxAge: THOUSAND_DAYS_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, refreshToken, options); +}; + +export const removeRefreshTokenCookie = ( + res: express.Response, + provider: string, +) => { + const options: CookieOptions = { + maxAge: 0, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, '', options); +}; diff --git a/plugins/auth-backend/src/providers/OAuthProvider.ts b/plugins/auth-backend/src/providers/OAuthProvider.ts new file mode 100644 index 0000000000..d294654453 --- /dev/null +++ b/plugins/auth-backend/src/providers/OAuthProvider.ts @@ -0,0 +1,116 @@ +import { AuthProviderRouteHandlers, OAuthProviderHandlers } from './types'; +import express from 'express'; +import { InputError } from '@backstage/backend-common'; +import { + setNonceCookie, + verifyNonce, + setRefreshTokenCookie, + removeRefreshTokenCookie, +} from './OAuthHelper'; +import { postMessageResponse, ensuresXRequestedWith } from './utils'; + +export class OAuthProvider implements AuthProviderRouteHandlers { + private readonly provider: string; + private readonly providerHandlers: OAuthProviderHandlers; + constructor(providerHandlers: OAuthProviderHandlers, provider: string) { + this.provider = provider; + this.providerHandlers = providerHandlers; + } + + async start(req: express.Request, res: express.Response): Promise { + // retrieve scopes from request + const scope = req.query.scope?.toString() ?? ''; + + if (!scope) { + throw new InputError('missing scope parameter'); + } + + // set a nonce cookie before redirecting to oauth provider + const nonce = setNonceCookie(res, this.provider); + + const options = { + scope, + accessType: 'offline', + prompt: 'consent', + state: nonce, + }; + const { url, status } = await this.providerHandlers.start(req, options); + + res.statusCode = status || 302; + res.setHeader('Location', url); + res.setHeader('Content-Length', '0'); + res.end(); + } + + async frameHandler( + req: express.Request, + res: express.Response, + ): Promise { + try { + // verify nonce cookie and state cookie on callback + verifyNonce(req, this.provider); + + const { user, info } = await this.providerHandlers.handler(req); + + // throw error if missing refresh token + const { refreshToken } = info; + if (!refreshToken) { + throw new Error('Missing refresh token'); + } + + // set new refresh token + setRefreshTokenCookie(res, this.provider, refreshToken); + + // post message back to popup if successful + return postMessageResponse(res, { + type: 'auth-result', + payload: user, + }); + } catch (error) { + // post error message back to popup if failure + return postMessageResponse(res, { + type: 'auth-result', + error: { + name: error.name, + message: error.message, + }, + }); + } + } + + async logout(req: express.Request, res: express.Response): Promise { + if (!ensuresXRequestedWith(req)) { + return res.status(401).send('Invalid X-Requested-With header'); + } + + // remove refresh token cookie before logout + removeRefreshTokenCookie(res, this.provider); + return res.send('logout!'); + } + + async refresh(req: express.Request, res: express.Response): Promise { + if (!ensuresXRequestedWith(req)) { + return res.status(401).send('Invalid X-Requested-With header'); + } + + try { + const refreshToken = req.cookies[`${this.provider}-refresh-token`]; + + // throw error if refresh token is missing in the request + if (!refreshToken) { + throw new Error('Missing session cookie'); + } + + const scope = req.query.scope?.toString() ?? ''; + + // get new access_token + const refreshInfo = await this.providerHandlers.refresh( + refreshToken, + scope, + ); + res.send(refreshInfo); + } catch (error) { + res.status(401).send(`${error.message}`); + } + } +} diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts new file mode 100644 index 0000000000..e33c183381 --- /dev/null +++ b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts @@ -0,0 +1,94 @@ +import express, { CookieOptions } from 'express'; +import passport from 'passport'; +import { RedirectInfo, AuthInfoBase } from './types'; + +export const executeRedirectStrategy = async ( + req: express.Request, + providerStrategy: passport.Strategy, + options: any, +): Promise => { + return new Promise(resolve => { + const strategy = Object.create(providerStrategy); + strategy.redirect = (url: string, status?: number) => { + resolve({ url, status: status ?? undefined }); + }; + + strategy.authenticate(req, { ...options }); + }); +}; + +export const executeFrameHandlerStrategy = async ( + req: express.Request, + providerStrategy: passport.Strategy, +) => { + return new Promise<{ user: any; info: any }>((resolve, reject) => { + const strategy = Object.create(providerStrategy); + strategy.success = (user: any, info: any) => { + resolve({ user, info }); + }; + strategy.fail = ( + info: { type: 'success' | 'error'; message?: string }, + _status?: number, + ) => { + reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); + }; + strategy.error = (error: Error) => { + reject(new Error(`Authentication failed, ${error}`)); + }; + strategy.redirect = () => { + reject(new Error('Unexpected redirect')); + }; + + strategy.authenticate(req); + }); +}; + +export const executeRefreshTokenStrategy = async ( + providerstrategy: passport.Strategy, + refreshToken: string, + scope: string, +): Promise => { + return new Promise((resolve, reject) => { + const anyStrategy = providerstrategy as any; + const OAuth2 = anyStrategy._oauth2.constructor; + const oauth2 = new OAuth2( + anyStrategy._oauth2._clientId, + anyStrategy._oauth2._clientSecret, + anyStrategy._oauth2._baseSite, + anyStrategy._oauth2._authorizeUrl, + anyStrategy._refreshURL || anyStrategy._oauth2._accessTokenUrl, + anyStrategy._oauth2._customHeaders, + ); + + oauth2.getOAuthAccessToken( + refreshToken, + { + scope, + grant_type: 'refresh_token', + }, + ( + err: Error | null, + accessToken: string, + _refreshToken: string, + params: any, + ) => { + if (err) { + reject(new Error(`Failed to refresh access token ${err}`)); + } + if (!accessToken) { + reject( + new Error( + `Failed to refresh access token, no access token received`, + ), + ); + } + resolve({ + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }); + }, + ); + }); +}; diff --git a/plugins/auth-backend/src/providers/factories.test.ts b/plugins/auth-backend/src/providers/factories.test.ts index 1647f62682..cc31834889 100644 --- a/plugins/auth-backend/src/providers/factories.test.ts +++ b/plugins/auth-backend/src/providers/factories.test.ts @@ -1,51 +1,51 @@ -/* - * 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. - */ +// /* +// * Copyright 2020 Spotify AB +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ -import express from 'express'; -import passport from 'passport'; -import { AuthProvider, AuthProviderRouteHandlers } from './types'; -import { ProviderFactories } from './factories'; +// import express from 'express'; +// import passport from 'passport'; +// import { OAuthProviderHandlers } from './types'; +// import { ProviderFactories } from './factories'; -class MyAuthProvider implements AuthProvider, AuthProviderRouteHandlers { - strategy(): passport.Strategy { - return new passport.Strategy(); - } - async start(_: express.Request, res: express.Response): Promise { - res.send('start'); - } - async frameHandler(_: express.Request, res: express.Response): Promise { - res.send('frameHandler'); - } - async logout(_: express.Request, res: express.Response): Promise { - res.send('logout'); - } -} +// class MyAuthProvider implements OAuthProviderHandlers { +// async start(_: express.Request, res: express.Response): Promise { +// res.send('start'); +// } +// async logout(_: express.Request, res: express.Response): Promise { +// res.send('logout'); +// } +// async handler(): Promise { +// throw new Error('Method not implemented.'); +// } +// async refresh(): Promise { +// throw new Error('Method not implemented.'); +// } +// } -describe('getProviderFactory', () => { - it('makes a provider for MyAuthProvider', () => { - jest - .spyOn(ProviderFactories, 'getProviderFactory') - .mockReturnValueOnce(MyAuthProvider); - const provider = ProviderFactories.getProviderFactory('a'); - expect(provider).toBeDefined(); - }); +// describe('getProviderFactory', () => { +// it('makes a provider for MyAuthProvider', () => { +// jest +// .spyOn(ProviderFactories, 'getProviderFactory') +// .mockReturnValueOnce(MyAuthProvider); +// const provider = ProviderFactories.getProviderFactory('a'); +// expect(provider).toBeDefined(); +// }); - it('throws an error when provider implementation does not exist', () => { - expect(() => { - ProviderFactories.getProviderFactory('b'); - }).toThrow('Provider Implementation missing for : b auth provider'); - }); -}); +// it('throws an error when provider implementation does not exist', () => { +// expect(() => { +// ProviderFactories.getProviderFactory('b'); +// }).toThrow('Provider Implementation missing for : b auth provider'); +// }); +// }); diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 077d45076e..76c3bd3dde 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -15,7 +15,8 @@ */ import { AuthProviderFactories, AuthProviderFactory } from './types'; -import { GoogleAuthProvider } from './google/provider'; +// import { GoogleAuthProvider } from './google/provider'; +import { GoogleAuthProvider } from './GoogleAuthProvider'; export class ProviderFactories { private static readonly providerFactories: AuthProviderFactories = { diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts new file mode 100644 index 0000000000..d79c9e34e9 --- /dev/null +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -0,0 +1 @@ +// export { GoogleAuthProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index 327bf0260b..03f3da1eda 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -1,522 +1,531 @@ -/* - * 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. - */ +// /* +// * Copyright 2020 Spotify AB +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ -import { - GoogleAuthProvider, - THOUSAND_DAYS_MS, - TEN_MINUTES_MS, -} from './provider'; -import passport from 'passport'; -import express from 'express'; -import * as utils from './../utils'; -import refresh from 'passport-oauth2-refresh'; +// import { +// GoogleAuthProvider, +// THOUSAND_DAYS_MS, +// TEN_MINUTES_MS, +// } from './provider'; +// import passport from 'passport'; +// import express from 'express'; +// import * as utils from './../utils'; +// import refresh from 'passport-oauth2-refresh'; -const googleAuthProviderConfig = { - provider: 'google', - options: { - clientID: 'a', - clientSecret: 'b', - callbackURL: 'c', - }, -}; +// const googleAuthProviderConfig = { +// provider: 'google', +// options: { +// clientID: 'a', +// clientSecret: 'b', +// callbackURL: 'c', +// }, +// }; -const googleAuthProviderConfigInvalidOptions = { - provider: 'google', - options: {}, -}; +// const googleAuthProviderConfigInvalidOptions = { +// provider: 'google', +// options: {}, +// }; -describe('GoogleAuthProvider', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - describe('create a new provider', () => { - it('should succeed with valid config', () => { - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - expect(googleAuthProvider).toBeDefined(); - expect(googleAuthProvider.start).toBeDefined(); - expect(googleAuthProvider.logout).toBeDefined(); - expect(googleAuthProvider.frameHandler).toBeDefined(); - expect(googleAuthProvider.strategy).toBeDefined(); - }); - }); +// describe('GoogleAuthProvider', () => { +// afterEach(() => { +// jest.clearAllMocks(); +// }); +// describe('create a new provider', () => { +// it('should succeed with valid config', () => { +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); +// expect(googleAuthProvider).toBeDefined(); +// expect(googleAuthProvider.start).toBeDefined(); +// expect(googleAuthProvider.logout).toBeDefined(); +// expect(googleAuthProvider.frameHandler).toBeDefined(); +// expect(googleAuthProvider.strategy).toBeDefined(); +// }); +// }); - describe('start authentication handler', () => { - const mockResponse = ({ - send: jest.fn().mockReturnThis(), - status: jest.fn().mockReturnThis(), - cookie: jest.fn().mockReturnThis(), - } as unknown) as express.Response; - const mockNext: express.NextFunction = jest.fn(); +// describe('start authentication handler', () => { +// const mockResponse = ({ +// send: jest.fn().mockReturnThis(), +// status: jest.fn().mockReturnThis(), +// cookie: jest.fn().mockReturnThis(), +// } as unknown) as express.Response; - it('should initiate authenticate request with provided scopes', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - query: { - scope: 'a,b', - }, - } as unknown) as express.Request; +// fit('should initiate authenticate request with provided scopes', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// query: { +// scope: 'a,b', +// }, +// } as unknown) as express.Request; - const spyPassport = jest - .spyOn(passport, 'authenticate') - .mockImplementation(() => jest.fn()); +// // const spyPassport = jest +// // .spyOn(passport, 'authenticate') +// // .mockImplementation(() => jest.fn()); +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - googleAuthProvider.start(mockRequest, mockResponse, mockNext); - expect(spyPassport).toBeCalledTimes(1); - expect(spyPassport).toBeCalledWith('google', { - scope: 'a,b', - accessType: 'offline', - prompt: 'consent', - state: expect.any(String), - }); - }); +// const googleAuthProviderStrategy = googleAuthProvider.strategy(); +// const spyAuthenticate = jest +// .spyOn(googleAuthProviderStrategy, 'authenticate') +// .mockImplementation(() => jest.fn()); - it('should set a nonce cookie', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - query: { - scope: 'a,b', - }, - } as unknown) as express.Request; +// // const spyRedirect = jest +// // .spyOn(googleAuthProviderStrategy, 'redirect') +// // .mockImplementation(() => jest.fn()); - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - googleAuthProvider.start(mockRequest, mockResponse, mockNext); - expect(mockResponse.cookie).toBeCalledTimes(1); - expect(mockResponse.cookie).toBeCalledWith( - 'google-nonce', - expect.any(String), - expect.objectContaining({ - maxAge: TEN_MINUTES_MS, - path: `/auth/${googleAuthProviderConfig.provider}/handler`, - }), - ); - }); +// googleAuthProvider.start(mockRequest, mockResponse); +// expect(spyAuthenticate).toBeCalledTimes(1); +// expect(spyAuthenticate).toBeCalledWith(mockRequest, { +// scope: 'a,b', +// accessType: 'offline', +// prompt: 'consent', +// state: expect.any(String), +// }); +// // expect(spyRedirect).toBeCalledTimes(1); +// // expect(spyPassport).toBeCalledTimes(1); +// }); - it('should throw error if no scopes provided', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - query: {}, - } as unknown) as express.Request; +// it('should set a nonce cookie', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// query: { +// scope: 'a,b', +// }, +// } as unknown) as express.Request; - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); - expect(() => { - googleAuthProvider.start(mockRequest, mockResponse, mockNext); - }).toThrowError('missing scope parameter'); - }); - }); +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); +// googleAuthProvider.start(mockRequest, mockResponse); +// expect(mockResponse.cookie).toBeCalledTimes(1); +// expect(mockResponse.cookie).toBeCalledWith( +// 'google-nonce', +// expect.any(String), +// expect.objectContaining({ +// maxAge: TEN_MINUTES_MS, +// path: `/auth/${googleAuthProviderConfig.provider}/handler`, +// }), +// ); +// }); - describe('logout handler', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - } as unknown) as express.Request; +// it('should throw error if no scopes provided', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// query: {}, +// } as unknown) as express.Request; - it('should perform logout and respond with 200', () => { - const mockResponse: any = ({ - send: jest.fn(), - cookie: jest.fn(), - } as unknown) as express.Response; +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); +// expect(() => { +// googleAuthProvider.start(mockRequest, mockResponse); +// }).toThrowError('missing scope parameter'); +// }); +// }); - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// describe('logout handler', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// } as unknown) as express.Request; - const spyResponse = jest - .spyOn(mockResponse, 'send') - .mockImplementation(() => jest.fn()); +// it('should perform logout and respond with 200', () => { +// const mockResponse: any = ({ +// send: jest.fn(), +// cookie: jest.fn(), +// } as unknown) as express.Response; - googleAuthProvider.logout(mockRequest, mockResponse); - expect(spyResponse).toBeCalledTimes(1); - expect(spyResponse).toBeCalledWith('logout!'); - expect(mockResponse.cookie).toBeCalledTimes(1); - expect(mockResponse.cookie).toBeCalledWith( - 'google-refresh-token', - '', - expect.objectContaining({ maxAge: 0 }), - ); - }); - }); +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - describe('redirect frame handler', () => { - const mockResponse: any = ({ - status: jest.fn().mockReturnThis(), - send: jest.fn().mockReturnThis(), - cookie: jest.fn().mockReturnThis(), - } as unknown) as express.Response; - const mockNext: express.NextFunction = jest.fn(); +// const spyResponse = jest +// .spyOn(mockResponse, 'send') +// .mockImplementation(() => jest.fn()); - it('should call authenticate and post a response', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-nonce': 'NONCE' }, - query: { - state: 'NONCE', - }, - } as unknown) as express.Request; +// googleAuthProvider.logout(mockRequest, mockResponse); +// expect(spyResponse).toBeCalledTimes(1); +// expect(spyResponse).toBeCalledWith('logout!'); +// expect(mockResponse.cookie).toBeCalledTimes(1); +// expect(mockResponse.cookie).toBeCalledWith( +// 'google-refresh-token', +// '', +// expect.objectContaining({ maxAge: 0 }), +// ); +// }); +// }); - const spyPostMessage = jest - .spyOn(utils, 'postMessageResponse') - .mockImplementation(() => jest.fn()); +// describe('redirect frame handler', () => { +// const mockResponse: any = ({ +// status: jest.fn().mockReturnThis(), +// send: jest.fn().mockReturnThis(), +// cookie: jest.fn().mockReturnThis(), +// } as unknown) as express.Response; - const spyPassport = jest - .spyOn(passport, 'authenticate') - .mockImplementation((_x, callbackFunc) => { - const cb = callbackFunc as Function; - cb(null, { refreshToken: 'REFRESH_TOKEN' }); - return jest.fn(); - }); +// it('should call authenticate and post a response', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// cookies: { 'google-nonce': 'NONCE' }, +// query: { +// state: 'NONCE', +// }, +// } as unknown) as express.Request; - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// const spyPostMessage = jest +// .spyOn(utils, 'postMessageResponse') +// .mockImplementation(() => jest.fn()); - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(spyPassport).toBeCalledTimes(1); - expect(spyPostMessage).toBeCalledTimes(1); - expect(mockResponse.cookie).toBeCalledTimes(1); - expect(mockResponse.cookie).toBeCalledWith( - 'google-refresh-token', - 'REFRESH_TOKEN', - expect.objectContaining({ - path: '/auth/google', - sameSite: 'none', - httpOnly: true, - maxAge: THOUSAND_DAYS_MS, - }), - ); - }); +// const spyPassport = jest +// .spyOn(passport, 'authenticate') +// .mockImplementation((_x, callbackFunc) => { +// const cb = callbackFunc as Function; +// cb(null, { refreshToken: 'REFRESH_TOKEN' }); +// return jest.fn(); +// }); - it('should respond with a error message if no refresh token returned', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-nonce': 'NONCE' }, - query: { - state: 'NONCE', - }, - } as unknown) as express.Request; +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - const spyPassport = jest - .spyOn(passport, 'authenticate') - .mockImplementation((_x, callbackFunc) => { - const cb = callbackFunc as Function; - cb(null, {}); - return jest.fn(); - }); +// googleAuthProvider.frameHandler(mockRequest, mockResponse); +// expect(spyPassport).toBeCalledTimes(1); +// expect(spyPostMessage).toBeCalledTimes(1); +// expect(mockResponse.cookie).toBeCalledTimes(1); +// expect(mockResponse.cookie).toBeCalledWith( +// 'google-refresh-token', +// 'REFRESH_TOKEN', +// expect.objectContaining({ +// path: '/auth/google', +// sameSite: 'none', +// httpOnly: true, +// maxAge: THOUSAND_DAYS_MS, +// }), +// ); +// }); - const spyPostMessage = jest - .spyOn(utils, 'postMessageResponse') - .mockImplementation(() => jest.fn()); +// it('should respond with a error message if no refresh token returned', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// cookies: { 'google-nonce': 'NONCE' }, +// query: { +// state: 'NONCE', +// }, +// } as unknown) as express.Request; - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// const spyPassport = jest +// .spyOn(passport, 'authenticate') +// .mockImplementation((_x, callbackFunc) => { +// const cb = callbackFunc as Function; +// cb(null, {}); +// return jest.fn(); +// }); - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(spyPassport).toBeCalledTimes(1); - expect(spyPostMessage).toBeCalledTimes(1); - expect(spyPostMessage).toBeCalledWith(mockResponse, { - type: 'auth-result', - error: new Error('Missing refresh token'), - }); - }); +// const spyPostMessage = jest +// .spyOn(utils, 'postMessageResponse') +// .mockImplementation(() => jest.fn()); - it('should respond with a error message if auth failed', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-nonce': 'NONCE' }, - query: { - state: 'NONCE', - }, - } as unknown) as express.Request; +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - const spyPassport = jest - .spyOn(passport, 'authenticate') - .mockImplementation((_x, callbackFunc) => { - const cb = callbackFunc as Function; - cb(new Error('TokenError'), null); - return jest.fn(); - }); +// googleAuthProvider.frameHandler(mockRequest, mockResponse); +// expect(spyPassport).toBeCalledTimes(1); +// expect(spyPostMessage).toBeCalledTimes(1); +// expect(spyPostMessage).toBeCalledWith(mockResponse, { +// type: 'auth-result', +// error: new Error('Missing refresh token'), +// }); +// }); - const spyPostMessage = jest - .spyOn(utils, 'postMessageResponse') - .mockImplementation(() => jest.fn()); +// it('should respond with a error message if auth failed', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// cookies: { 'google-nonce': 'NONCE' }, +// query: { +// state: 'NONCE', +// }, +// } as unknown) as express.Request; - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// const spyPassport = jest +// .spyOn(passport, 'authenticate') +// .mockImplementation((_x, callbackFunc) => { +// const cb = callbackFunc as Function; +// cb(new Error('TokenError'), null); +// return jest.fn(); +// }); - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(spyPassport).toBeCalledTimes(1); - expect(spyPostMessage).toBeCalledTimes(1); - expect(spyPostMessage).toBeCalledWith(mockResponse, { - type: 'auth-result', - error: new Error('Google auth failed, Error: TokenError'), - }); - }); +// const spyPostMessage = jest +// .spyOn(utils, 'postMessageResponse') +// .mockImplementation(() => jest.fn()); - it('should respond with a error message if cookie nonce is missing', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: {}, - query: { state: 'NONCE' }, - } as unknown) as express.Request; +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// googleAuthProvider.frameHandler(mockRequest, mockResponse); +// expect(spyPassport).toBeCalledTimes(1); +// expect(spyPostMessage).toBeCalledTimes(1); +// expect(spyPostMessage).toBeCalledWith(mockResponse, { +// type: 'auth-result', +// error: new Error('Google auth failed, Error: TokenError'), +// }); +// }); - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith('Missing nonce'); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - }); +// it('should respond with a error message if cookie nonce is missing', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// cookies: {}, +// query: { state: 'NONCE' }, +// } as unknown) as express.Request; - it('should respond with a error message if state nonce is missing', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-nonce': 'NONCE' }, - query: {}, - } as unknown) as express.Request; +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// googleAuthProvider.frameHandler(mockRequest, mockResponse); +// expect(mockResponse.send).toBeCalledTimes(1); +// expect(mockResponse.send).toBeCalledWith('Missing nonce'); +// expect(mockResponse.status).toBeCalledTimes(1); +// expect(mockResponse.status).toBeCalledWith(401); +// }); - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith('Missing nonce'); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - }); +// it('should respond with a error message if state nonce is missing', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// cookies: { 'google-nonce': 'NONCE' }, +// query: {}, +// } as unknown) as express.Request; - it('should respond with a error message if nonce mismatch', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-nonce': 'NONCA' }, - query: { state: 'NONCEB' }, - } as unknown) as express.Request; +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// googleAuthProvider.frameHandler(mockRequest, mockResponse); +// expect(mockResponse.send).toBeCalledTimes(1); +// expect(mockResponse.send).toBeCalledWith('Missing nonce'); +// expect(mockResponse.status).toBeCalledTimes(1); +// expect(mockResponse.status).toBeCalledWith(401); +// }); - googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith('Invalid nonce'); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - }); - }); +// it('should respond with a error message if nonce mismatch', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// cookies: { 'google-nonce': 'NONCA' }, +// query: { state: 'NONCEB' }, +// } as unknown) as express.Request; - describe('strategy handler', () => { - it('should return a valid passport strategy', () => { - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - expect(googleAuthProvider.strategy()).toBeInstanceOf(passport.Strategy); - }); +// googleAuthProvider.frameHandler(mockRequest, mockResponse); +// expect(mockResponse.send).toBeCalledTimes(1); +// expect(mockResponse.send).toBeCalledWith('Invalid nonce'); +// expect(mockResponse.status).toBeCalledTimes(1); +// expect(mockResponse.status).toBeCalledWith(401); +// }); +// }); - it('should throw an error for invalid options', () => { - expect(() => { - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfigInvalidOptions, - ); - googleAuthProvider.strategy(); - }).toThrow(); - }); - }); +// describe('strategy handler', () => { +// it('should return a valid passport strategy', () => { +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - describe('refresh token handler', () => { - const mockResponse = ({ - status: jest.fn().mockReturnThis(), - send: jest.fn().mockReturnThis(), - } as unknown) as express.Response; +// expect(googleAuthProvider.strategy()).toBeInstanceOf(passport.Strategy); +// }); - describe('no refresh token cookie', () => { - it('should respond with a 401', () => { - const mockRequest = ({ - cookies: jest.fn(), - header: () => 'XMLHttpRequest', - } as unknown) as express.Request; +// it('should throw an error for invalid options', () => { +// expect(() => { +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfigInvalidOptions, +// ); +// googleAuthProvider.strategy(); +// }).toThrow(); +// }); +// }); - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// describe('refresh token handler', () => { +// const mockResponse = ({ +// status: jest.fn().mockReturnThis(), +// send: jest.fn().mockReturnThis(), +// } as unknown) as express.Response; - googleAuthProvider.refresh(mockRequest, mockResponse); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith('Missing session cookie'); +// describe('no refresh token cookie', () => { +// it('should respond with a 401', () => { +// const mockRequest = ({ +// cookies: jest.fn(), +// header: () => 'XMLHttpRequest', +// } as unknown) as express.Request; - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - }); - }); +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - describe('refresh token cookie, no scope', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, - query: {}, - } as unknown) as express.Request; +// googleAuthProvider.refresh(mockRequest, mockResponse); +// expect(mockResponse.send).toBeCalledTimes(1); +// expect(mockResponse.send).toBeCalledWith('Missing session cookie'); - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// expect(mockResponse.status).toBeCalledTimes(1); +// expect(mockResponse.status).toBeCalledWith(401); +// }); +// }); - it('should request for a new access token and fail if no access token returned', () => { - const spyRefresh = jest - .spyOn(refresh, 'requestNewAccessToken') - .mockImplementation((_x, _y, _z, callbackFunc) => { - const cb = callbackFunc as Function; - cb(undefined, undefined, undefined, {}); - }); +// describe('refresh token cookie, no scope', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, +// query: {}, +// } as unknown) as express.Request; - googleAuthProvider.refresh(mockRequest, mockResponse); - expect(spyRefresh).toBeCalledTimes(1); - expect(spyRefresh).toBeCalledWith( - 'google', - 'REFRESH_TOKEN', - {}, - expect.any(Function), - ); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith( - 'Failed to refresh access token', - ); - }); +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - it('should request for a new access token and return 401 if any error', () => { - const spyRefresh = jest - .spyOn(refresh, 'requestNewAccessToken') - .mockImplementation((_x, _y, _z, callbackFunc) => { - const cb = callbackFunc as Function; - cb({ error: 'ERROR' }, undefined, undefined, {}); - }); +// it('should request for a new access token and fail if no access token returned', () => { +// const spyRefresh = jest +// .spyOn(refresh, 'requestNewAccessToken') +// .mockImplementation((_x, _y, _z, callbackFunc) => { +// const cb = callbackFunc as Function; +// cb(undefined, undefined, undefined, {}); +// }); - googleAuthProvider.refresh(mockRequest, mockResponse); - expect(spyRefresh).toBeCalledTimes(1); - expect(spyRefresh).toBeCalledWith( - 'google', - 'REFRESH_TOKEN', - {}, - expect.any(Function), - ); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith( - 'Failed to refresh access token', - ); - }); +// googleAuthProvider.refresh(mockRequest, mockResponse); +// expect(spyRefresh).toBeCalledTimes(1); +// expect(spyRefresh).toBeCalledWith( +// 'google', +// 'REFRESH_TOKEN', +// {}, +// expect.any(Function), +// ); +// expect(mockResponse.status).toBeCalledTimes(1); +// expect(mockResponse.status).toBeCalledWith(401); +// expect(mockResponse.send).toBeCalledTimes(1); +// expect(mockResponse.send).toBeCalledWith( +// 'Failed to refresh access token', +// ); +// }); - it('should fetch and return a new access token', () => { - const spyRefresh = jest - .spyOn(refresh, 'requestNewAccessToken') - .mockImplementation((_x, _y, _z, callbackFunc) => { - const cb = callbackFunc as Function; - cb(undefined, 'ACCESS_TOKEN', undefined, { - expires_in: 'EXPIRES_IN', - id_token: 'ID_TOKEN', - }); - }); +// it('should request for a new access token and return 401 if any error', () => { +// const spyRefresh = jest +// .spyOn(refresh, 'requestNewAccessToken') +// .mockImplementation((_x, _y, _z, callbackFunc) => { +// const cb = callbackFunc as Function; +// cb({ error: 'ERROR' }, undefined, undefined, {}); +// }); - googleAuthProvider.refresh(mockRequest, mockResponse); - expect(spyRefresh).toBeCalledTimes(1); - expect(spyRefresh).toBeCalledWith( - 'google', - 'REFRESH_TOKEN', - {}, - expect.any(Function), - ); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith({ - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 'EXPIRES_IN', - scope: undefined, - }); - }); - }); +// googleAuthProvider.refresh(mockRequest, mockResponse); +// expect(spyRefresh).toBeCalledTimes(1); +// expect(spyRefresh).toBeCalledWith( +// 'google', +// 'REFRESH_TOKEN', +// {}, +// expect.any(Function), +// ); +// expect(mockResponse.status).toBeCalledTimes(1); +// expect(mockResponse.status).toBeCalledWith(401); +// expect(mockResponse.send).toBeCalledTimes(1); +// expect(mockResponse.send).toBeCalledWith( +// 'Failed to refresh access token', +// ); +// }); - describe('refresh token cookie and scope', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, - query: { - scope: 'a,b', - }, - } as unknown) as express.Request; +// it('should fetch and return a new access token', () => { +// const spyRefresh = jest +// .spyOn(refresh, 'requestNewAccessToken') +// .mockImplementation((_x, _y, _z, callbackFunc) => { +// const cb = callbackFunc as Function; +// cb(undefined, 'ACCESS_TOKEN', undefined, { +// expires_in: 'EXPIRES_IN', +// id_token: 'ID_TOKEN', +// }); +// }); - const googleAuthProvider = new GoogleAuthProvider( - googleAuthProviderConfig, - ); +// googleAuthProvider.refresh(mockRequest, mockResponse); +// expect(spyRefresh).toBeCalledTimes(1); +// expect(spyRefresh).toBeCalledWith( +// 'google', +// 'REFRESH_TOKEN', +// {}, +// expect.any(Function), +// ); +// expect(mockResponse.send).toBeCalledTimes(1); +// expect(mockResponse.send).toBeCalledWith({ +// accessToken: 'ACCESS_TOKEN', +// idToken: 'ID_TOKEN', +// expiresInSeconds: 'EXPIRES_IN', +// scope: undefined, +// }); +// }); +// }); - it('should fetch and return a new access token with scopes', () => { - const spyRefresh = jest - .spyOn(refresh, 'requestNewAccessToken') - .mockImplementation((_x, _y, _z, callbackFunc) => { - const cb = callbackFunc as Function; - cb(undefined, 'ACCESS_TOKEN', undefined, { - expires_in: 'EXPIRES_IN', - id_token: 'ID_TOKEN', - scope: 'a,b', - }); - }); +// describe('refresh token cookie and scope', () => { +// const mockRequest = ({ +// header: () => 'XMLHttpRequest', +// cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, +// query: { +// scope: 'a,b', +// }, +// } as unknown) as express.Request; - googleAuthProvider.refresh(mockRequest, mockResponse); - expect(spyRefresh).toBeCalledTimes(1); - expect(spyRefresh).toBeCalledWith( - 'google', - 'REFRESH_TOKEN', - { scope: 'a,b' }, - expect.any(Function), - ); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith({ - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 'EXPIRES_IN', - scope: 'a,b', - }); - }); +// const googleAuthProvider = new GoogleAuthProvider( +// googleAuthProviderConfig, +// ); - it('ensures x-requested-with header', () => { - const mockHeaderRequest = ({ - header: () => 'TEST', - } as unknown) as express.Request; +// it('should fetch and return a new access token with scopes', () => { +// const spyRefresh = jest +// .spyOn(refresh, 'requestNewAccessToken') +// .mockImplementation((_x, _y, _z, callbackFunc) => { +// const cb = callbackFunc as Function; +// cb(undefined, 'ACCESS_TOKEN', undefined, { +// expires_in: 'EXPIRES_IN', +// id_token: 'ID_TOKEN', +// scope: 'a,b', +// }); +// }); - googleAuthProvider.refresh(mockHeaderRequest, mockResponse); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith( - 'Invalid X-Requested-With header', - ); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); - }); - }); - }); -}); +// googleAuthProvider.refresh(mockRequest, mockResponse); +// expect(spyRefresh).toBeCalledTimes(1); +// expect(spyRefresh).toBeCalledWith( +// 'google', +// 'REFRESH_TOKEN', +// { scope: 'a,b' }, +// expect.any(Function), +// ); +// expect(mockResponse.send).toBeCalledTimes(1); +// expect(mockResponse.send).toBeCalledWith({ +// accessToken: 'ACCESS_TOKEN', +// idToken: 'ID_TOKEN', +// expiresInSeconds: 'EXPIRES_IN', +// scope: 'a,b', +// }); +// }); + +// it('ensures x-requested-with header', () => { +// const mockHeaderRequest = ({ +// header: () => 'TEST', +// } as unknown) as express.Request; + +// googleAuthProvider.refresh(mockHeaderRequest, mockResponse); +// expect(mockResponse.send).toBeCalledTimes(1); +// expect(mockResponse.send).toBeCalledWith( +// 'Invalid X-Requested-With header', +// ); +// expect(mockResponse.status).toBeCalledTimes(1); +// expect(mockResponse.status).toBeCalledWith(401); +// }); +// }); +// }); +// }); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index cb080e2fd3..a090f7f950 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -1,198 +1,151 @@ -/* - * 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. - */ +// /* +// * Copyright 2020 Spotify AB +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ -import passport from 'passport'; -import express, { CookieOptions } from 'express'; -import crypto from 'crypto'; -import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -import refresh from 'passport-oauth2-refresh'; -import { - AuthProvider, - AuthProviderRouteHandlers, - AuthProviderConfig, -} from './../types'; -import { postMessageResponse, ensuresXRequestedWith } from './../utils'; -import { InputError } from '@backstage/backend-common'; +// import passport from 'passport'; +// import express from 'express'; +// import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; +// import { +// AuthProvider, +// AuthProviderRouteHandlers, +// AuthProviderConfig, +// } from './../types'; +// import { postMessageResponse, ensuresXRequestedWith } from './../utils'; +// import { InputError } from '@backstage/backend-common'; -export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; -export const TEN_MINUTES_MS = 600 * 1000; -export class GoogleAuthProvider - implements AuthProvider, AuthProviderRouteHandlers { - private readonly providerConfig: AuthProviderConfig; - constructor(providerConfig: AuthProviderConfig) { - this.providerConfig = providerConfig; - } +// export class GoogleAuthProvider +// implements AuthProvider, AuthProviderRouteHandlers { +// private readonly provider: string; +// private readonly providerConfig: AuthProviderConfig; +// private readonly _strategy: GoogleStrategy; - start( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ) { - const nonce = crypto.randomBytes(16).toString('base64'); +// constructor(handler: OAuthProviderHandlers) { +// this.provider = providerConfig.provider; +// this.providerConfig = providerConfig; +// // TODO: throw error if env variables not set? +// this._strategy = new GoogleStrategy( +// { ...this.providerConfig.options }, +// ( +// accessToken: any, +// refreshToken: any, +// params: any, +// profile: any, +// done: any, +// ) => { +// done( +// undefined, +// { +// profile, +// idToken: params.id_token, +// accessToken, +// scope: params.scope, +// expiresInSeconds: params.expires_in, +// }, +// { +// refreshToken, +// }, +// ); +// }, +// ); +// } - const options: CookieOptions = { - maxAge: TEN_MINUTES_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${this.providerConfig.provider}/handler`, - httpOnly: true, - }; +// async start(req: express.Request, res: express.Response) { +// const scope = req.query.scope?.toString() ?? ''; - res.cookie(`${this.providerConfig.provider}-nonce`, nonce, options); +// if (!scope) { +// throw new InputError('missing scope parameter'); +// } - const scope = req.query.scope?.toString() ?? ''; - if (!scope) { - throw new InputError('missing scope parameter'); - } - return passport.authenticate('google', { - scope, - accessType: 'offline', - prompt: 'consent', - state: nonce, - })(req, res, next); - } +// // router -> [AuthProviderRouteHandlers] -> OAuthProvider -> [OAuthProviderHandler] -> GoogleAuthProvider +// // router -> [AuthProviderRouteHandlers] -> GoogleAuthProvider - frameHandler( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ) { - const cookieNonce = req.cookies[`${this.providerConfig.provider}-nonce`]; - const stateNonce = req.query.state; +// // class GoogleAuthProvider2 implements OAuthProviderHandler { +// // async start(req: express.Request): Promise { - if (!cookieNonce || !stateNonce) { - return res.status(401).send('Missing nonce'); - } +// // } +// // async handler(req: express.Request): Promise { +// // const { user, info } = await executeFrameHandlerStrategy( +// // req, +// // this.provider, +// // this._strategy, +// // ); +// // return { user, info } +// // } +// // } - if (cookieNonce !== stateNonce) { - return res.status(401).send('Invalid nonce'); - } +// executeRedirectStrategy(req, res, this.provider, this._strategy, { +// scope, +// accessType: 'offline', +// prompt: 'consent', +// }); +// } - return passport.authenticate('google', (err, user) => { - if (err) { - return postMessageResponse(res, { - type: 'auth-result', - error: new Error(`Google auth failed, ${err}`), - }); - } +// async frameHandler(req: express.Request, res: express.Response) { +// try { +// // const { user, info } = await this.handler.handler(req); +// const { user, info } = await executeFrameHandlerStrategy(req); - const { refreshToken } = user; +// const { refreshToken } = info; +// if (!refreshToken) { +// throw new Error('Missing refresh token'); +// } - if (!refreshToken) { - return postMessageResponse(res, { - type: 'auth-result', - error: new Error('Missing refresh token'), - }); - } +// setRefreshTokenCookie(res, this.provider, refreshToken); - delete user.refreshToken; +// return postMessageResponse(res, { +// type: 'auth-result', +// payload: user, +// }); +// } catch (error) { +// return postMessageResponse(res, { +// type: 'auth-result', +// error: { +// name: error.name, +// message: error.message, +// }, +// }); +// } +// } - const options: CookieOptions = { - maxAge: THOUSAND_DAYS_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${this.providerConfig.provider}`, - httpOnly: true, - }; +// async logout(req: express.Request, res: express.Response) { +// if (!ensuresXRequestedWith(req)) { +// return res.status(401).send('Invalid X-Requested-With header'); +// } - res.cookie( - `${this.providerConfig.provider}-refresh-token`, - refreshToken, - options, - ); - return postMessageResponse(res, { - type: 'auth-result', - payload: user, - }); - })(req, res, next); - } +// removeRefreshTokenCookie(res, this.provider); +// return res.send('logout!'); +// } - async logout(req: express.Request, res: express.Response) { - if (!ensuresXRequestedWith(req)) { - return res.status(401).send('Invalid X-Requested-With header'); - } +// async refresh(req: express.Request, res: express.Response) { +// if (!ensuresXRequestedWith(req)) { +// return res.status(401).send('Invalid X-Requested-With header'); +// } - const options: CookieOptions = { - maxAge: 0, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${this.providerConfig.provider}`, - httpOnly: true, - }; +// try { +// const refreshInfo = await executeRefreshTokenStrategy( +// req, +// this.provider, +// this._strategy, +// ); +// res.send(refreshInfo); +// } catch (error) { +// res.status(401).send(`${error.message}`); +// } +// } - res.cookie(`${this.providerConfig.provider}-refresh-token`, '', options); - return res.send('logout!'); - } - - async refresh(req: express.Request, res: express.Response) { - if (!ensuresXRequestedWith(req)) { - return res.status(401).send('Invalid X-Requested-With header'); - } - - const refreshToken = - req.cookies[`${this.providerConfig.provider}-refresh-token`]; - - if (!refreshToken) { - return res.status(401).send('Missing session cookie'); - } - - const scope = req.query.scope?.toString() ?? ''; - const refreshTokenRequestParams = scope ? { scope } : {}; - - return refresh.requestNewAccessToken( - this.providerConfig.provider, - refreshToken, - refreshTokenRequestParams, - (err, accessToken, _refreshToken, params) => { - if (err || !accessToken) { - return res.status(401).send('Failed to refresh access token'); - } - return res.send({ - accessToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, - }); - }, - ); - } - - strategy(): passport.Strategy { - // TODO: throw error if env variables not set? - return new GoogleStrategy( - { ...this.providerConfig.options }, - ( - accessToken: any, - refreshToken: any, - params: any, - profile: any, - done: any, - ) => { - done(undefined, { - profile, - idToken: params.id_token, - accessToken, - refreshToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }); - }, - ); - } -} +// strategy(): passport.Strategy { +// return this._strategy; +// } +// } diff --git a/plugins/auth-backend/src/providers/index.test.ts b/plugins/auth-backend/src/providers/index.test.ts index e42cd32edc..74f7657c44 100644 --- a/plugins/auth-backend/src/providers/index.test.ts +++ b/plugins/auth-backend/src/providers/index.test.ts @@ -1,103 +1,100 @@ -/* - * 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. - */ +// /* +// * Copyright 2020 Spotify AB +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ -import passport from 'passport'; -import express from 'express'; -import { makeProvider, defaultRouter } from '.'; -import { - AuthProvider, - AuthProviderRouteHandlers, - AuthProviderConfig, -} from './types'; -import * as passportGoogleOAuth20 from 'passport-google-oauth20'; -import { ProviderFactories } from './factories'; +// import passport from 'passport'; +// import express from 'express'; +// import { makeProvider, defaultRouter } from '.'; +// import { AuthProviderRouteHandlers, AuthProviderConfig } from './types'; +// import * as passportGoogleOAuth20 from 'passport-google-oauth20'; +// import { ProviderFactories } from './factories'; -class MyAuthProvider implements AuthProvider, AuthProviderRouteHandlers { - private readonly providerConfig: AuthProviderConfig; - constructor(providerConfig: AuthProviderConfig) { - this.providerConfig = providerConfig; - } +// class MyOAuthProvider implements AuthProviderHandlers {} - strategy(): passport.Strategy { - return new passportGoogleOAuth20.Strategy( - this.providerConfig.options, - () => {}, - ); - } - async start(_: express.Request, res: express.Response): Promise { - res.send('start'); - } - async frameHandler(_: express.Request, res: express.Response): Promise { - res.send('frameHandler'); - } - async logout(_: express.Request, res: express.Response): Promise { - res.send('logout'); - } -} +// class MyAuthProvider implements AuthProviderRouteHandlers { +// // private readonly providerConfig: AuthProviderConfig; +// constructor(providerConfig: AuthProviderConfig) { +// this.providerConfig = providerConfig; +// } -class MyAuthProviderWithRefresh extends MyAuthProvider { - async refresh(_: express.Request, res: express.Response): Promise { - res.send('logout'); - } -} +// strategy(): passport.Strategy { +// return new passportGoogleOAuth20.Strategy( +// this.providerConfig.options, +// () => {}, +// ); +// } +// async start(_: express.Request, res: express.Response): Promise { +// res.send('start'); +// } +// async frameHandler(_: express.Request, res: express.Response): Promise { +// res.send('frameHandler'); +// } +// async logout(_: express.Request, res: express.Response): Promise { +// res.send('logout'); +// } +// } -const providerConfig = { - provider: 'a', - options: { - clientID: 'somevalue', - }, -}; +// class MyAuthProviderWithRefresh extends MyAuthProvider { +// async refresh(_: express.Request, res: express.Response): Promise { +// res.send('logout'); +// } +// } -const providerConfigInvalid = { - provider: 'b', - options: { - clientID: 'somevalue', - }, -}; +// const providerConfig = { +// provider: 'a', +// options: { +// clientID: 'somevalue', +// }, +// }; -describe('makeProvider', () => { - it('makes a provider for Myauthprovider', () => { - jest - .spyOn(ProviderFactories, 'getProviderFactory') - .mockReturnValueOnce(MyAuthProvider); - const provider = makeProvider(providerConfig); - expect(provider.providerId).toEqual('a'); - expect(provider.strategy).toBeDefined(); - expect(provider.providerRouter).toBeDefined(); - }); +// const providerConfigInvalid = { +// provider: 'b', +// options: { +// clientID: 'somevalue', +// }, +// }; - it('throws an error when provider implementation does not exist', () => { - expect(() => { - makeProvider(providerConfigInvalid); - }).toThrow('Provider Implementation missing for : b auth provider'); - }); -}); +// describe('makeProvider', () => { +// it('makes a provider for Myauthprovider', () => { +// jest +// .spyOn(ProviderFactories, 'getProviderFactory') +// .mockReturnValueOnce(MyAuthProvider); +// const provider = makeProvider(providerConfig); +// expect(provider.providerId).toEqual('a'); +// expect(provider.providerRouter).toBeDefined(); +// }); -describe('defaultRouter', () => { - it('make router for auth provider without refresh', () => { - expect( - defaultRouter(new MyAuthProvider({ provider: 'a', options: {} })), - ).toBeDefined(); - }); +// it('throws an error when provider implementation does not exist', () => { +// expect(() => { +// makeProvider(providerConfigInvalid); +// }).toThrow('Provider Implementation missing for : b auth provider'); +// }); +// }); - it('make router for auth provider with refresh', () => { - expect( - defaultRouter( - new MyAuthProviderWithRefresh({ provider: 'b', options: {} }), - ), - ).toBeDefined(); - }); -}); +// describe('defaultRouter', () => { +// it('make router for auth provider without refresh', () => { +// expect( +// defaultRouter(new MyAuthProvider({ provider: 'a', options: {} })), +// ).toBeDefined(); +// }); + +// it('make router for auth provider with refresh', () => { +// expect( +// defaultRouter( +// new MyAuthProviderWithRefresh({ provider: 'b', options: {} }), +// ), +// ).toBeDefined(); +// }); +// }); diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 1b33391c27..59dd116fa6 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -17,6 +17,7 @@ import Router from 'express-promise-router'; import { AuthProviderRouteHandlers, AuthProviderConfig } from './types'; import { ProviderFactories } from './factories'; +import { OAuthProvider } from './OAuthProvider'; export const defaultRouter = (provider: AuthProviderRouteHandlers) => { const router = Router(); @@ -33,7 +34,8 @@ export const makeProvider = (config: AuthProviderConfig) => { const providerId = config.provider; const ProviderImpl = ProviderFactories.getProviderFactory(providerId); const providerInstance = new ProviderImpl(config); - const strategy = providerInstance.strategy(); - const providerRouter = defaultRouter(providerInstance); - return { providerId, strategy, providerRouter }; + + const oauthProvider = new OAuthProvider(providerInstance, providerId); + const providerRouter = defaultRouter(oauthProvider); + return { providerId, providerRouter }; }; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 36941c6850..a9c43716e2 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -22,9 +22,15 @@ export type AuthProviderConfig = { options: any; }; -export interface AuthProvider { - strategy(): passport.Strategy; - router?(): express.Router; +export interface OAuthProviderHandlers { + start(req: express.Request, options: any): Promise; + handler(req: express.Request): Promise; + refresh(refreshToken: string, scope: string): Promise; + logout( + req: express.Request, + res: express.Response, + next: express.NextFunction, + ): Promise; } export interface AuthProviderRouteHandlers { @@ -55,7 +61,7 @@ export type AuthProviderFactories = { }; export type AuthProviderFactory = { - new (providerConfig: any): AuthProvider & AuthProviderRouteHandlers; + new (providerConfig: any): OAuthProviderHandlers; }; export type AuthInfoBase = { @@ -69,7 +75,7 @@ export type AuthInfoWithProfile = AuthInfoBase & { profile: passport.Profile; }; -export type AuthInfoPrivate = AuthInfoWithProfile & { +export type AuthInfoPrivate = { refreshToken: string; }; @@ -82,3 +88,8 @@ export type AuthResponse = type: 'auth-result'; error: Error; }; + +export type RedirectInfo = { + url: string; + status?: number; +}; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index a2e3b3e1db..03d197f50b 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -16,10 +16,7 @@ import express from 'express'; import Router from 'express-promise-router'; -import passport from 'passport'; import cookieParser from 'cookie-parser'; -import refresh from 'passport-oauth2-refresh'; -import OAuth2Strategy from 'passport-oauth2'; import { Logger } from 'winston'; import { providers } from './../providers/config'; import { makeProvider } from '../providers'; @@ -33,38 +30,13 @@ export async function createRouter( ): Promise { const router = Router(); const logger = options.logger.child({ plugin: 'auth' }); - const providerRouters: { [key: string]: express.Router } = {}; + + router.use(cookieParser()); // configure all the providers for (const providerConfig of providers) { - const { providerId, strategy, providerRouter } = makeProvider( - providerConfig, - ); - logger.info(`Configuring provider: ${providerId}`); - passport.use(strategy); - if (strategy instanceof OAuth2Strategy) { - refresh.use(strategy); - } - providerRouters[providerId] = providerRouter; - } - - passport.serializeUser((user, done) => { - done(null, user); - }); - - passport.deserializeUser((user, done) => { - done(null, user); - }); - - router.use(passport.initialize()); - router.use(passport.session()); - router.use(cookieParser()); - - for (const providerId in providerRouters) { - if (providerRouters.hasOwnProperty(providerId)) { - const providerRouter = providerRouters[providerId]; - router.use(`/${providerId}`, providerRouter); - } + const { providerId, providerRouter } = makeProvider(providerConfig); + router.use(`/${providerId}`, providerRouter); } return router; From 3673722f69a1ec1fbe81b8e20b6386ca1f6fec6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 11:52:50 +0200 Subject: [PATCH 12/97] Move the model into a plugin-catalog-model for sharing outside the backend --- packages/backend/package.json | 13 +- packages/backend/src/plugins/catalog.ts | 14 +- packages/catalog-model/.eslintrc.js | 3 + packages/catalog-model/README.md | 12 + packages/catalog-model/package.json | 34 +++ packages/catalog-model/src/EntityPolicies.ts | 88 ++++++++ packages/catalog-model/src/entity/Entity.ts | 108 +++++++++ .../catalog-model/src/entity}/index.ts | 6 +- .../policies/FieldFormatEntityPolicy.test.ts | 105 +++++++++ .../policies/FieldFormatEntityPolicy.ts | 91 ++++++++ .../ForeignRootFieldsEntityPolicy.test.ts | 52 +++++ .../policies/ForeignRootFieldsEntityPolicy.ts | 40 ++++ .../ReservedFieldsEntityPolicy.test.ts | 62 ++++++ .../policies/ReservedFieldsEntityPolicy.ts | 66 ++++++ .../policies/SchemaValidEntityPolicy.test.ts | 176 +++++++++++++++ .../policies/SchemaValidEntityPolicy.ts | 80 +++++++ .../src/entity/policies/index.ts | 20 ++ packages/catalog-model/src/index.ts | 21 ++ .../src/kinds/ComponentV1beta1.ts | 32 +-- packages/catalog-model/src/kinds/index.ts | 20 ++ packages/catalog-model/src/setupTests.ts | 15 ++ packages/catalog-model/src/types.ts | 32 +++ .../CommonValidatorFunctions.test.ts | 0 .../validation/CommonValidatorFunctions.ts | 0 .../KubernetesValidatorFunctions.test.ts | 0 .../KubernetesValidatorFunctions.ts | 0 .../catalog-model/src/validation/index.ts | 20 ++ .../src/validation/makeValidator.ts | 0 .../catalog-model}/src/validation/types.ts | 0 plugins/catalog-backend/package.json | 5 +- .../src/catalog/DatabaseEntitiesCatalog.ts | 8 +- .../catalog/DatabaseLocationsCatalog.test.ts | 40 ++-- .../src/catalog/DatabaseLocationsCatalog.ts | 14 +- .../src/catalog/StaticEntitiesCatalog.ts | 12 +- plugins/catalog-backend/src/catalog/types.ts | 8 +- .../src/database/Database.test.ts | 10 +- .../catalog-backend/src/database/Database.ts | 14 +- .../src/database/DatabaseManager.test.ts | 82 ++++--- .../src/database/DatabaseManager.ts | 32 +-- ...0200520140700_location_update_log_table.ts | 5 +- .../src/database/search.test.ts | 6 +- .../catalog-backend/src/database/search.ts | 4 +- plugins/catalog-backend/src/database/types.ts | 6 +- .../src/ingestion/DescriptorParsers.ts | 53 ----- .../src/ingestion/IngestionModels.ts | 73 +++++++ .../src/ingestion/LocationReaders.ts | 39 ---- .../ingestion/descriptor/DescriptorParsers.ts | 45 ++++ .../src/ingestion/descriptor/index.ts | 18 ++ .../parsers/YamlDescriptorParser.ts | 64 ++++++ .../src/ingestion/descriptor/parsers/types.ts | 42 ++++ .../DescriptorEnvelopeParser.test.ts | 172 --------------- .../descriptors/DescriptorEnvelopeParser.ts | 206 ------------------ .../catalog-backend/src/ingestion/index.ts | 7 +- .../src/ingestion/source/LocationReaders.ts | 41 ++++ .../src/ingestion/source/index.ts | 20 ++ .../readers/FileLocationReader.ts} | 23 +- .../readers/GitHubLocationReader.test.ts} | 52 ++--- .../readers/GitHubLocationReader.ts} | 47 ++-- .../src/ingestion/source/readers/types.ts | 29 +++ .../src/ingestion/sources/util.ts | 55 ----- .../catalog-backend/src/ingestion/types.ts | 170 +-------------- .../src/service/router.test.ts | 8 +- plugins/catalog-backend/src/service/router.ts | 2 +- yarn.lock | 5 - 64 files changed, 1602 insertions(+), 925 deletions(-) create mode 100644 packages/catalog-model/.eslintrc.js create mode 100644 packages/catalog-model/README.md create mode 100644 packages/catalog-model/package.json create mode 100644 packages/catalog-model/src/EntityPolicies.ts create mode 100644 packages/catalog-model/src/entity/Entity.ts rename {plugins/catalog-backend/src/validation => packages/catalog-model/src/entity}/index.ts (79%) create mode 100644 packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts create mode 100644 packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts create mode 100644 packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts create mode 100644 packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts create mode 100644 packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts create mode 100644 packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts create mode 100644 packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts create mode 100644 packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts create mode 100644 packages/catalog-model/src/entity/policies/index.ts create mode 100644 packages/catalog-model/src/index.ts rename plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts => packages/catalog-model/src/kinds/ComponentV1beta1.ts (62%) create mode 100644 packages/catalog-model/src/kinds/index.ts create mode 100644 packages/catalog-model/src/setupTests.ts create mode 100644 packages/catalog-model/src/types.ts rename {plugins/catalog-backend => packages/catalog-model}/src/validation/CommonValidatorFunctions.test.ts (100%) rename {plugins/catalog-backend => packages/catalog-model}/src/validation/CommonValidatorFunctions.ts (100%) rename {plugins/catalog-backend => packages/catalog-model}/src/validation/KubernetesValidatorFunctions.test.ts (100%) rename {plugins/catalog-backend => packages/catalog-model}/src/validation/KubernetesValidatorFunctions.ts (100%) create mode 100644 packages/catalog-model/src/validation/index.ts rename {plugins/catalog-backend => packages/catalog-model}/src/validation/makeValidator.ts (100%) rename {plugins/catalog-backend => packages/catalog-model}/src/validation/types.ts (100%) delete mode 100644 plugins/catalog-backend/src/ingestion/DescriptorParsers.ts create mode 100644 plugins/catalog-backend/src/ingestion/IngestionModels.ts delete mode 100644 plugins/catalog-backend/src/ingestion/LocationReaders.ts create mode 100644 plugins/catalog-backend/src/ingestion/descriptor/DescriptorParsers.ts create mode 100644 plugins/catalog-backend/src/ingestion/descriptor/index.ts create mode 100644 plugins/catalog-backend/src/ingestion/descriptor/parsers/YamlDescriptorParser.ts create mode 100644 plugins/catalog-backend/src/ingestion/descriptor/parsers/types.ts delete mode 100644 plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.test.ts delete mode 100644 plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts create mode 100644 plugins/catalog-backend/src/ingestion/source/LocationReaders.ts create mode 100644 plugins/catalog-backend/src/ingestion/source/index.ts rename plugins/catalog-backend/src/ingestion/{sources/FileLocationSource.ts => source/readers/FileLocationReader.ts} (63%) rename plugins/catalog-backend/src/ingestion/{sources/__tests__/GitHubLocationSource.test.ts => source/readers/GitHubLocationReader.test.ts} (64%) rename plugins/catalog-backend/src/ingestion/{sources/GitHubLocationSource.ts => source/readers/GitHubLocationReader.ts} (70%) create mode 100644 plugins/catalog-backend/src/ingestion/source/readers/types.ts delete mode 100644 plugins/catalog-backend/src/ingestion/sources/util.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index 9aedafecbb..51fe04e51d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -10,7 +10,7 @@ }, "scripts": { "build": "tsc", - "start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess nodemon", + "start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"nodemon -r esm\\\"", "lint": "backstage-cli lint", "test": "backstage-cli test", "clean": "backstage-cli clean", @@ -18,13 +18,15 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.6", + "@backstage/catalog-model": "^0.1.1-alpha.6", "@backstage/plugin-auth-backend": "^0.1.1-alpha.6", "@backstage/plugin-catalog-backend": "^0.1.1-alpha.6", - "@backstage/plugin-sentry-backend": "^0.1.1-alpha.6", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.6", "@backstage/plugin-identity-backend": "^0.1.1-alpha.6", + "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.6", + "@backstage/plugin-sentry-backend": "^0.1.1-alpha.6", "compression": "^1.7.4", "cors": "^2.8.5", + "esm": "^3.2.25", "express": "^4.17.1", "helmet": "^3.22.0", "knex": "^0.21.1", @@ -43,6 +45,9 @@ "typescript": "^3.9.2" }, "nodemonConfig": { - "watch": "./dist" + "watch": [ + "./dist", + "node_modules/@backstage*" + ] } } diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 687fd9157a..7e843cc80b 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -21,22 +21,28 @@ import { DatabaseManager, DescriptorParsers, LocationReaders, + IngestionModels, runPeriodically, } from '@backstage/plugin-catalog-backend'; import { PluginEnvironment } from '../types'; +import { EntityPolicies } from '@backstage/catalog-model'; export default async function ({ logger, database }: PluginEnvironment) { - const reader = LocationReaders.create(); - const parser = DescriptorParsers.create(); + const policy = new EntityPolicies(); + const ingestion = new IngestionModels( + new LocationReaders(), + new DescriptorParsers(), + new EntityPolicies(), + ); const db = await DatabaseManager.createDatabase(database, logger); runPeriodically( - () => DatabaseManager.refreshLocations(db, reader, parser, logger), + () => DatabaseManager.refreshLocations(db, ingestion, policy, logger), 10000, ); const entitiesCatalog = new DatabaseEntitiesCatalog(db); - const locationsCatalog = new DatabaseLocationsCatalog(db, reader); + const locationsCatalog = new DatabaseLocationsCatalog(db, ingestion); return await createRouter({ entitiesCatalog, locationsCatalog, logger }); } diff --git a/packages/catalog-model/.eslintrc.js b/packages/catalog-model/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/catalog-model/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/catalog-model/README.md b/packages/catalog-model/README.md new file mode 100644 index 0000000000..755b9ee63c --- /dev/null +++ b/packages/catalog-model/README.md @@ -0,0 +1,12 @@ +# Catalog Model + +Contains the core model types and validators/policies used by the Backstage catalog functionality. + +This package will be imported both by the frontend and backend parts of the catalog, +as well as by others that want to consume catalog data. + +## Links + +- (Default frontend part of the catalog)[https://github.com/spotify/backstage/tree/master/plugins/catalog] +- (Default backend part of the catalog)[https://github.com/spotify/backstage/tree/master/plugins/catalog-backend] +- (The Backstage homepage)[https://backstage.io] diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json new file mode 100644 index 0000000000..ffd8fdba22 --- /dev/null +++ b/packages/catalog-model/package.json @@ -0,0 +1,34 @@ +{ + "name": "@backstage/catalog-model", + "version": "0.1.1-alpha.6", + "main": "dist/index.esm.js", + "main:src": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "lodash": "^4.17.15", + "yup": "^0.28.5" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.6", + "@types/jest": "^25.2.2", + "@types/lodash": "^4.14.151", + "@types/yup": "^0.28.2", + "yaml": "^1.9.2" + }, + "files": [ + "dist/**/*.{js,d.ts}" + ] +} diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts new file mode 100644 index 0000000000..cedf0df13d --- /dev/null +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Entity, + FieldFormatEntityPolicy, + ForeignRootFieldsEntityPolicy, + ReservedFieldsEntityPolicy, + SchemaValidEntityPolicy, +} from './entity'; +import { ComponentV1beta1Policy } from './kinds'; +import { EntityPolicy } from './types'; + +// Helper that requires that all of a set of policies can be successfully +// applied +class AllEntityPolicies implements EntityPolicy { + constructor(private readonly policies: EntityPolicy[]) {} + + async apply(entity: Entity): Promise { + let result = entity; + for (const policy of this.policies) { + result = await policy.apply(entity); + } + return result; + } +} + +// Helper that requires that at least one of a set of policies can be +// successfully applied +class AnyEntityPolicy implements EntityPolicy { + constructor(private readonly policies: EntityPolicy[]) {} + + async apply(entity: Entity): Promise { + for (const policy of this.policies) { + try { + return await policy.apply(entity); + } catch { + continue; + } + } + throw new Error(`The entity did not match any known policy`); + } +} + +export class EntityPolicies implements EntityPolicy { + private readonly policy: EntityPolicy; + + static defaultPolicies(): EntityPolicy { + return EntityPolicies.allOf([ + EntityPolicies.allOf([ + new SchemaValidEntityPolicy(), + new ForeignRootFieldsEntityPolicy(), + new FieldFormatEntityPolicy(), + new ReservedFieldsEntityPolicy(), + ]), + EntityPolicies.anyOf([new ComponentV1beta1Policy()]), + ]); + } + + static allOf(policies: EntityPolicy[]): EntityPolicy { + return new AllEntityPolicies(policies); + } + + static anyOf(policies: EntityPolicy[]): EntityPolicy { + return new AnyEntityPolicy(policies); + } + + constructor(policy: EntityPolicy = EntityPolicies.defaultPolicies()) { + this.policy = policy; + } + + apply(entity: Entity): Promise { + return this.policy.apply(entity); + } +} diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts new file mode 100644 index 0000000000..6f57626749 --- /dev/null +++ b/packages/catalog-model/src/entity/Entity.ts @@ -0,0 +1,108 @@ +/* + * 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. + */ + +/** + * The format envelope that's common to all versions/kinds of entity. + * + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ + */ +export type Entity = { + /** + * The version of specification format for this particular entity that + * this is written against. + */ + apiVersion: string; + + /** + * The high level entity type being described. + */ + kind: string; + + /** + * Optional metadata related to the entity. + */ + metadata?: EntityMeta; + + /** + * The specification data describing the entity itself. + */ + spec?: object; +}; + +/** + * Metadata fields common to all versions/kinds of entity. + * + * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ + */ +export type EntityMeta = { + /** + * A globally unique ID for the entity. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. The field can (optionally) be specified when performing + * update or delete operations, but the server is free to reject requests + * that do so in such a way that it breaks semantics. + */ + uid?: string; + + /** + * An opaque string that changes for each update operation to any part of + * the entity, including metadata. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. The field can (optionally) be specified when performing + * update or delete operations, and the server will then reject the + * operation if it does not match the current stored value. + */ + etag?: string; + + /** + * A positive nonzero number that indicates the current generation of data + * for this entity; the value is incremented each time the spec changes. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. + */ + generation?: number; + + /** + * The name of the entity. + * + * Must be uniqe within the catalog at any given point in time, for any + * given namespace, for any given kind. + */ + name?: string; + + /** + * The namespace that the entity belongs to. + */ + namespace?: string; + + /** + * Key/value pairs of identifying information attached to the entity. + */ + labels?: Record; + + /** + * Key/value pairs of non-identifying auxiliary information attached to the + * entity. + */ + annotations?: Record; +}; diff --git a/plugins/catalog-backend/src/validation/index.ts b/packages/catalog-model/src/entity/index.ts similarity index 79% rename from plugins/catalog-backend/src/validation/index.ts rename to packages/catalog-model/src/entity/index.ts index be607e43ec..9e96021336 100644 --- a/plugins/catalog-backend/src/validation/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -14,7 +14,5 @@ * limitations under the License. */ -export * from './CommonValidatorFunctions'; -export * from './KubernetesValidatorFunctions'; -export * from './makeValidator'; -export * from './types'; +export type { Entity, EntityMeta } from './Entity'; +export * from './policies'; diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts new file mode 100644 index 0000000000..d81b1155be --- /dev/null +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; +import { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy'; + +describe('FieldFormatEntityPolicy', () => { + let data: any; + let policy: FieldFormatEntityPolicy; + + beforeEach(() => { + data = yaml.parse(` + apiVersion: backstage.io/v1beta1 + kind: Component + metadata: + uid: e01199ab-08cc-44c2-8e19-5c29ded82521 + etag: lsndfkjsndfkjnsdfkjnsd== + generation: 13 + name: my-component-yay + namespace: the-namespace + labels: + backstage.io/custom: ValueStuff + annotations: + example.com/bindings: are-secret + spec: + custom: stuff + `); + policy = new FieldFormatEntityPolicy(); + }); + + it('works for the happy path', async () => { + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad apiVersion', async () => { + data.apiVersion = 7; + await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + data.apiVersion = 'a#b'; + await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + }); + + it('rejects bad kind', async () => { + data.kind = 7; + await expect(policy.apply(data)).rejects.toThrow(/kind/); + data.kind = 'a#b'; + await expect(policy.apply(data)).rejects.toThrow(/kind/); + }); + + it('handles missing metadata gracefully', async () => { + delete data.medatata; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('handles missing spec gracefully', async () => { + delete data.spec; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad name', async () => { + data.metadata.name = 7; + await expect(policy.apply(data)).rejects.toThrow(/name.*7/); + data.metadata.name = 'a'.repeat(1000); + await expect(policy.apply(data)).rejects.toThrow(/name.*aaaa/); + }); + + it('rejects bad namespace', async () => { + data.metadata.namespace = 7; + await expect(policy.apply(data)).rejects.toThrow(/namespace.*7/); + data.metadata.namespace = 'a'.repeat(1000); + await expect(policy.apply(data)).rejects.toThrow(/namespace.*aaaa/); + }); + + it('rejects bad label key', async () => { + data.metadata.labels['a#b'] = 'value'; + await expect(policy.apply(data)).rejects.toThrow(/label.*a#b/i); + }); + + it('rejects bad label value', async () => { + data.metadata.labels.a = 'a#b'; + await expect(policy.apply(data)).rejects.toThrow(/label.*a#b/i); + }); + + it('rejects bad annotation key', async () => { + data.metadata.annotations['a#b'] = 'value'; + await expect(policy.apply(data)).rejects.toThrow(/annotation.*a#b/i); + }); + + it('rejects bad annotation value', async () => { + data.metadata.annotations.a = 7; + await expect(policy.apply(data)).rejects.toThrow(/annotation.*7/i); + }); +}); diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts new file mode 100644 index 0000000000..1f94354f3f --- /dev/null +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityPolicy } from '../../types'; +import { makeValidator, Validators } from '../../validation'; +import { Entity } from '../Entity'; + +/** + * Ensures that the format of individual fields of the entity envelope + * is valid. + * + * This does not take into account machine generated fields such as uid, etag + * and generation. + */ +export class FieldFormatEntityPolicy implements EntityPolicy { + private readonly validators: Validators; + + constructor(validators: Validators = makeValidator()) { + this.validators = validators; + } + + async apply(entity: Entity): Promise { + function require( + field: string, + value: any, + validator: (value: any) => boolean, + ) { + if (value === undefined || value === null) { + throw new Error(`${field} must have a value`); + } + + let isValid: boolean; + try { + isValid = validator(value); + } catch (e) { + throw new Error(`${field} could not be validated, ${e}`); + } + + if (!isValid) { + throw new Error(`${field} "${value}" is not valid`); + } + } + + function optional( + field: string, + value: any, + validator: (value: any) => boolean, + ) { + return value === undefined || require(field, value, validator); + } + + require('apiVersion', entity.apiVersion, this.validators.isValidApiVersion); + require('kind', entity.kind, this.validators.isValidKind); + + optional( + 'metadata.name', + entity.metadata?.name, + this.validators.isValidEntityName, + ); + optional( + 'metadata.namespace', + entity.metadata?.namespace, + this.validators.isValidNamespace, + ); + + for (const [k, v] of Object.entries(entity.metadata?.labels ?? [])) { + require(`labels.${k}`, k, this.validators.isValidLabelKey); + require(`labels.${k}`, v, this.validators.isValidLabelValue); + } + + for (const [k, v] of Object.entries(entity.metadata?.annotations ?? [])) { + require(`annotations.${k}`, k, this.validators.isValidAnnotationKey); + require(`annotations.${k}`, v, this.validators.isValidAnnotationValue); + } + + return entity; + } +} diff --git a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts new file mode 100644 index 0000000000..98299259f8 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; +import { ForeignRootFieldsEntityPolicy } from './ForeignRootFieldsEntityPolicy'; + +describe('ForeignRootFieldsEntityPolicy', () => { + let data: any; + let policy: ForeignRootFieldsEntityPolicy; + + beforeEach(() => { + data = yaml.parse(` + apiVersion: backstage.io/v1beta1 + kind: Component + metadata: + uid: e01199ab-08cc-44c2-8e19-5c29ded82521 + etag: lsndfkjsndfkjnsdfkjnsd== + generation: 13 + name: my-component-yay + namespace: the-namespace + labels: + backstage.io/custom: ValueStuff + annotations: + example.com/bindings: are-secret + spec: + custom: stuff + `); + policy = new ForeignRootFieldsEntityPolicy(); + }); + + it('works for the happy path', async () => { + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects unknown root fields', async () => { + data.spec2 = {}; + await expect(policy.apply(data)).rejects.toThrow(/spec2/i); + }); +}); diff --git a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts new file mode 100644 index 0000000000..a4733e9a42 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityPolicy } from '../../types'; +import { Entity } from '../Entity'; + +const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec']; + +/** + * Ensures that there are no foreign root fields in the entity. + */ +export class ForeignRootFieldsEntityPolicy implements EntityPolicy { + private readonly knownFields: string[]; + + constructor(knownFields: string[] = defaultKnownFields) { + this.knownFields = knownFields; + } + + async apply(entity: Entity): Promise { + for (const field of Object.keys(entity)) { + if (!this.knownFields.includes(field)) { + throw new Error(`Unknown field ${field}`); + } + } + return entity; + } +} diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts new file mode 100644 index 0000000000..348eabbdac --- /dev/null +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; +import { ReservedFieldsEntityPolicy } from './ReservedFieldsEntityPolicy'; + +describe('ReservedFieldsEntityPolicy', () => { + let data: any; + let policy: ReservedFieldsEntityPolicy; + + beforeEach(() => { + data = yaml.parse(` + apiVersion: backstage.io/v1beta1 + kind: Component + metadata: + uid: e01199ab-08cc-44c2-8e19-5c29ded82521 + etag: lsndfkjsndfkjnsdfkjnsd== + generation: 13 + name: my-component-yay + namespace: the-namespace + labels: + backstage.io/custom: ValueStuff + annotations: + example.com/bindings: are-secret + spec: + custom: stuff + `); + policy = new ReservedFieldsEntityPolicy(); + }); + + it('works for the happy path', async () => { + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects reserved keys in the spec root', async () => { + data.spec.apiVersion = 'a/b'; + await expect(policy.apply(data)).rejects.toThrow(/spec.*apiVersion/i); + }); + + it('rejects reserved keys in labels', async () => { + data.metadata.labels.apiVersion = 'a'; + await expect(policy.apply(data)).rejects.toThrow(/label.*apiVersion/i); + }); + + it('rejects reserved keys in annotations', async () => { + data.metadata.annotations.apiVersion = 'a'; + await expect(policy.apply(data)).rejects.toThrow(/annotation.*apiVersion/i); + }); +}); diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts new file mode 100644 index 0000000000..be2f732ca4 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts @@ -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. + */ + +import { EntityPolicy } from '../../types'; +import { Entity } from '../Entity'; + +const DEFAULT_RESERVED_ENTITY_FIELDS = [ + 'apiVersion', + 'kind', + 'uid', + 'etag', + 'generation', + 'name', + 'namespace', + 'labels', + 'annotations', + 'spec', +]; + +/** + * Ensures that fields are not given certain reserved names. + */ +export class ReservedFieldsEntityPolicy implements EntityPolicy { + private readonly reservedFields: string[]; + + constructor(fields?: string[]) { + this.reservedFields = [ + ...(fields ?? []), + ...DEFAULT_RESERVED_ENTITY_FIELDS, + ]; + } + + async apply(entity: Entity): Promise { + for (const field of this.reservedFields) { + if (entity.spec?.hasOwnProperty(field)) { + throw new Error( + `The spec may not contain the field ${field}, because it has reserved meaning`, + ); + } + if (entity.metadata?.labels?.hasOwnProperty(field)) { + throw new Error( + `A label may not have the field ${field}, because it has reserved meaning`, + ); + } + if (entity.metadata?.annotations?.hasOwnProperty(field)) { + throw new Error( + `An annotation may not have the field ${field}, because it has reserved meaning`, + ); + } + } + return entity; + } +} diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts new file mode 100644 index 0000000000..b9d1165a60 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -0,0 +1,176 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; +import { Entity } from '../Entity'; +import { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy'; + +describe('SchemaValidEntityPolicy', () => { + let data: any; + let policy: SchemaValidEntityPolicy; + + beforeEach(() => { + data = yaml.parse(` + apiVersion: backstage.io/v1beta1 + kind: Component + metadata: + uid: e01199ab-08cc-44c2-8e19-5c29ded82521 + etag: lsndfkjsndfkjnsdfkjnsd== + generation: 13 + name: my-component-yay + namespace: the-namespace + labels: + backstage.io/custom: ValueStuff + annotations: + example.com/bindings: are-secret + spec: + custom: stuff + `); + policy = new SchemaValidEntityPolicy(); + }); + + it('works for the happy path', async () => { + await expect(policy.apply(data)).resolves.toBe(data); + }); + + // + // apiVersion and kind + // + + it('rejects wrong root type', async () => { + await expect(policy.apply((7 as unknown) as Entity)).rejects.toThrow( + /object/, + ); + }); + + it('rejects missing apiVersion', async () => { + delete data.apiVersion; + await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + }); + + it('rejects bad apiVersion type', async () => { + data.apiVersion = 7; + await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + }); + + it('rejects missing kind', async () => { + delete data.kind; + await expect(policy.apply(data)).rejects.toThrow(/kind/); + }); + + it('rejects bad kind type', async () => { + data.kind = 7; + await expect(policy.apply(data)).rejects.toThrow(/kind/); + }); + + // + // metadata + // + + it('accepts missing metadata', async () => { + delete data.medatata; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad metadata type', async () => { + data.metadata = 7; + await expect(policy.apply(data)).rejects.toThrow(/metadata/); + }); + + it('accepts missing uid', async () => { + delete data.metadata.uid; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad uid type', async () => { + data.metadata.uid = 7; + await expect(policy.apply(data)).rejects.toThrow(/uid/); + }); + + it('accepts missing etag', async () => { + delete data.metadata.etag; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad etag type', async () => { + data.metadata.etag = 7; + await expect(policy.apply(data)).rejects.toThrow(/etag/); + }); + + it('accepts missing generation', async () => { + delete data.metadata.generation; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad generation type', async () => { + data.metadata.generation = 'a'; + await expect(policy.apply(data)).rejects.toThrow(/generation/); + }); + + it('accepts missing name', async () => { + delete data.metadata.name; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad name type', async () => { + data.metadata.name = 7; + await expect(policy.apply(data)).rejects.toThrow(/name/); + }); + + it('accepts missing namespace', async () => { + delete data.metadata.namespace; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad namespace type', async () => { + data.metadata.namespace = 7; + await expect(policy.apply(data)).rejects.toThrow(/namespace/); + }); + + it('accepts missing labels', async () => { + delete data.metadata.labels; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad labels type', async () => { + data.metadata.labels = 7; + await expect(policy.apply(data)).rejects.toThrow(/labels/); + }); + + it('accepts missing annotations', async () => { + delete data.metadata.annotations; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad annotations type', async () => { + data.metadata.annotations = 7; + await expect(policy.apply(data)).rejects.toThrow(/annotations/); + }); + + // + // spec + // + + it('accepts missing spec', async () => { + delete data.spec; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects non-object spec', async () => { + data.spec = 7; + await expect(policy.apply(data)).rejects.toThrow(/spec/); + }); +}); diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts new file mode 100644 index 0000000000..7c0f5c20b6 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as yup from 'yup'; +import { EntityPolicy } from '../../types'; +import { Entity } from '../Entity'; + +const DEFAULT_ENTITY_SCHEMA = yup.object({ + apiVersion: yup.string().required(), + kind: yup.string().required(), + metadata: yup + .object({ + uid: yup + .string() + .notRequired() + .test( + 'metadata.uid', + 'The uid must not be empty', + value => value === undefined || value.length > 0, + ), + etag: yup + .string() + .notRequired() + .test( + 'metadata.etag', + 'The etag must not be empty', + value => value === undefined || value.length > 0, + ), + generation: yup + .number() + .notRequired() + .test( + 'metadata.generation', + 'The generation must be an integer greater than zero', + value => value === undefined || (value === (value | 0) && value > 0), + ), + name: yup.string().notRequired(), + namespace: yup.string().notRequired(), + labels: yup.object>().notRequired(), + annotations: yup.object>().notRequired(), + }) + .notRequired(), + spec: yup.object({}).notRequired(), +}); + +/** + * Ensures that the entity spec is valid according to a schema. + * + * This should be the first policy in the list, to ensure that other downstream + * policies can work with a structure that is at least valid in therms of the + * typescript type. + */ +export class SchemaValidEntityPolicy implements EntityPolicy { + private readonly schema: yup.Schema; + + constructor(schema: yup.Schema = DEFAULT_ENTITY_SCHEMA) { + this.schema = schema; + } + + async apply(entity: Entity): Promise { + try { + return await this.schema.validate(entity, { strict: true }); + } catch (e) { + throw new Error(`Malformed envelope, ${e}`); + } + } +} diff --git a/packages/catalog-model/src/entity/policies/index.ts b/packages/catalog-model/src/entity/policies/index.ts new file mode 100644 index 0000000000..f43aa68049 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy'; +export { ForeignRootFieldsEntityPolicy } from './ForeignRootFieldsEntityPolicy'; +export { ReservedFieldsEntityPolicy } from './ReservedFieldsEntityPolicy'; +export { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy'; diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts new file mode 100644 index 0000000000..fb51461053 --- /dev/null +++ b/packages/catalog-model/src/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export * from './entity'; +export { EntityPolicies } from './EntityPolicies'; +export * from './kinds'; +export type { EntityPolicy } from './types'; +export * from './validation'; diff --git a/plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts b/packages/catalog-model/src/kinds/ComponentV1beta1.ts similarity index 62% rename from plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts rename to packages/catalog-model/src/kinds/ComponentV1beta1.ts index 974b34aa8e..b0a3f627a5 100644 --- a/plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts +++ b/packages/catalog-model/src/kinds/ComponentV1beta1.ts @@ -15,19 +15,28 @@ */ import * as yup from 'yup'; -import { DescriptorEnvelope, KindParser, ParserError } from '../types'; +import type { Entity, EntityMeta } from '../entity/Entity'; +import type { EntityPolicy } from '../types'; -export interface ComponentDescriptorV1beta1 extends DescriptorEnvelope { +const API_VERSION = 'backstage.io/v1beta1'; +const KIND = 'Component'; + +export interface ComponentV1beta1 extends Entity { + apiVersion: typeof API_VERSION; + kind: typeof KIND; + metadata: EntityMeta & { + name: string; + }; spec: { type: string; }; } -export class ComponentDescriptorV1beta1Parser implements KindParser { +export class ComponentV1beta1Policy implements EntityPolicy { private schema: yup.Schema; constructor() { - this.schema = yup.object>({ + this.schema = yup.object>({ metadata: yup .object({ name: yup.string().required(), @@ -41,23 +50,14 @@ export class ComponentDescriptorV1beta1Parser implements KindParser { }); } - async tryParse( - envelope: DescriptorEnvelope, - ): Promise { + async apply(envelope: Entity): Promise { if ( envelope.apiVersion !== 'backstage.io/v1beta1' || envelope.kind !== 'Component' ) { - return undefined; + throw new Error('Unsupported apiVersion / kind'); } - try { - return await this.schema.validate(envelope, { strict: true }); - } catch (e) { - throw new ParserError( - `Malformed component, ${e}`, - envelope.metadata?.name, - ); - } + return await this.schema.validate(envelope, { strict: true }); } } diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts new file mode 100644 index 0000000000..97d22c14a5 --- /dev/null +++ b/packages/catalog-model/src/kinds/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ComponentV1beta1 } from './ComponentV1beta1'; +export { ComponentV1beta1Policy } from './ComponentV1beta1'; +export { ComponentV1beta1 as Component }; +export { ComponentV1beta1 }; diff --git a/packages/catalog-model/src/setupTests.ts b/packages/catalog-model/src/setupTests.ts new file mode 100644 index 0000000000..f3b69cc361 --- /dev/null +++ b/packages/catalog-model/src/setupTests.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts new file mode 100644 index 0000000000..1d581cf23f --- /dev/null +++ b/packages/catalog-model/src/types.ts @@ -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. + */ + +import type { Entity } from './entity/Entity'; + +/** + * A policy for validation or mutation to be applied to entities as they are + * entering the system. + */ +export type EntityPolicy = { + /** + * Applies validation or mutation on an entity. + * + * @param entity The entity, as validated/mutated so far in the policy tree + * @returns The incoming entity, or a mutated version of the same + * @throws An error if the entity should be rejected + */ + apply(entity: Entity): Promise; +}; diff --git a/plugins/catalog-backend/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts similarity index 100% rename from plugins/catalog-backend/src/validation/CommonValidatorFunctions.test.ts rename to packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts diff --git a/plugins/catalog-backend/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts similarity index 100% rename from plugins/catalog-backend/src/validation/CommonValidatorFunctions.ts rename to packages/catalog-model/src/validation/CommonValidatorFunctions.ts diff --git a/plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.test.ts b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.test.ts similarity index 100% rename from plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.test.ts rename to packages/catalog-model/src/validation/KubernetesValidatorFunctions.test.ts diff --git a/plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.ts b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts similarity index 100% rename from plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.ts rename to packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts diff --git a/packages/catalog-model/src/validation/index.ts b/packages/catalog-model/src/validation/index.ts new file mode 100644 index 0000000000..d679a5323c --- /dev/null +++ b/packages/catalog-model/src/validation/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { CommonValidatorFunctions } from './CommonValidatorFunctions'; +export { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions'; +export { makeValidator } from './makeValidator'; +export type { Validators } from './types'; diff --git a/plugins/catalog-backend/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts similarity index 100% rename from plugins/catalog-backend/src/validation/makeValidator.ts rename to packages/catalog-model/src/validation/makeValidator.ts diff --git a/plugins/catalog-backend/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts similarity index 100% rename from plugins/catalog-backend/src/validation/types.ts rename to packages/catalog-model/src/validation/types.ts diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index f32ffffb6c..5ec37295ed 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -16,8 +16,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.6", - "@types/node-fetch": "^2.5.7", - "@types/supertest": "^2.0.8", + "@backstage/catalog-model": "^0.1.1-alpha.6", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", @@ -38,6 +37,8 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", "@types/lodash": "^4.14.151", + "@types/node-fetch": "^2.5.7", + "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", "@types/yup": "^0.28.2", "jest-fetch-mock": "^3.0.3", diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 9d0d8fcaf7..972410a639 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -14,21 +14,21 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import { Database } from '../database'; -import { DescriptorEnvelope } from '../ingestion/types'; import { EntitiesCatalog, EntityFilters } from './types'; export class DatabaseEntitiesCatalog implements EntitiesCatalog { constructor(private readonly database: Database) {} - async entities(filters?: EntityFilters): Promise { + async entities(filters?: EntityFilters): Promise { const items = await this.database.transaction(tx => this.database.entities(tx, filters), ); return items.map(i => i.entity); } - async entityByUid(uid: string): Promise { + async entityByUid(uid: string): Promise { const matches = await this.database.transaction(tx => this.database.entities(tx, [{ key: 'uid', values: [uid] }]), ); @@ -40,7 +40,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { kind: string, name: string, namespace: string | undefined, - ): Promise { + ): Promise { const matches = await this.database.transaction(tx => this.database.entities(tx, [ { key: 'kind', values: [kind] }, diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts index 0181b7fc28..56a3b3828f 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -13,13 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; +import { getVoidLogger } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; import knex from 'knex'; import path from 'path'; - import { Database } from '../database'; -import { ReaderOutput } from '../ingestion/types'; -import { getVoidLogger } from '@backstage/backend-common'; +import { IngestionModel } from '../ingestion/types'; +import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; + +class MockIngestionModel implements IngestionModel { + readLocation = jest.fn(async (type: string, target: string) => { + if (type !== 'valid_type') { + throw new Error(`Unknown location type ${type}`); + } + if (target === 'valid_target') { + return [{ type: 'data', data: {} as Entity } as const]; + } + throw new Error( + `Can't read location at ${target} with error: Something is broken`, + ); + }); +} describe('DatabaseLocationsCatalog', () => { const database = knex({ @@ -32,20 +46,7 @@ describe('DatabaseLocationsCatalog', () => { }); let db: Database; let catalog: DatabaseLocationsCatalog; - - const mockLocationReader = { - read: async (type: string, target: string): Promise => { - if (type !== 'valid_type') { - throw new Error(`Unknown location type ${type}`); - } - if (target === 'valid_target') { - return Promise.resolve([{ type: 'data', data: {} }]); - } - throw new Error( - `Can't read location at ${target} with error: Something is broken`, - ); - }, - }; + let ingestionModel: IngestionModel; beforeEach(async () => { await database.migrate.latest({ @@ -53,7 +54,8 @@ describe('DatabaseLocationsCatalog', () => { loadExtensions: ['.ts'], }); db = new Database(database, getVoidLogger()); - catalog = new DatabaseLocationsCatalog(db, mockLocationReader); + ingestionModel = new MockIngestionModel(); + catalog = new DatabaseLocationsCatalog(db, ingestionModel); }); it('resolves to location with id', async () => { diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index 6e3c9ae306..b13d70de86 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -15,17 +15,25 @@ */ import { Database } from '../database'; +import { IngestionModel } from '../ingestion/types'; import { AddLocation, Location, LocationsCatalog } from './types'; -import { LocationReader } from '../ingestion'; export class DatabaseLocationsCatalog implements LocationsCatalog { constructor( private readonly database: Database, - private readonly reader: LocationReader, + private readonly ingestionModel: IngestionModel, ) {} async addLocation(location: AddLocation): Promise { - const outputs = await this.reader.read(location.type, location.target); + const outputs = await this.ingestionModel.readLocation( + location.type, + location.target, + ); + if (!outputs) { + throw new Error( + `Unknown location type ${location.type} ${location.target}`, + ); + } outputs.forEach(output => { if (output.type === 'error') { throw new Error( diff --git a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts index 371cdf1f76..1de606d44d 100644 --- a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts @@ -15,22 +15,22 @@ */ import { NotFoundError } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; import lodash from 'lodash'; -import { DescriptorEnvelope } from '../ingestion'; import { EntitiesCatalog } from './types'; export class StaticEntitiesCatalog implements EntitiesCatalog { - private _entities: DescriptorEnvelope[]; + private _entities: Entity[]; - constructor(entities: DescriptorEnvelope[]) { + constructor(entities: Entity[]) { this._entities = entities; } - async entities(): Promise { + async entities(): Promise { return lodash.cloneDeep(this._entities); } - async entityByUid(uid: string): Promise { + async entityByUid(uid: string): Promise { const item = this._entities.find(e => uid === e.metadata?.uid); if (!item) { throw new NotFoundError('Entity cannot be found'); @@ -42,7 +42,7 @@ export class StaticEntitiesCatalog implements EntitiesCatalog { kind: string, name: string, namespace: string | undefined, - ): Promise { + ): Promise { const item = this._entities.find( e => kind === e.kind && diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index d5627ea86a..2f8967cd1b 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import * as yup from 'yup'; -import { DescriptorEnvelope } from '../ingestion'; // // Entities @@ -28,13 +28,13 @@ export type EntityFilter = { export type EntityFilters = EntityFilter[]; export type EntitiesCatalog = { - entities(filters?: EntityFilters): Promise; - entityByUid(uid: string): Promise; + entities(filters?: EntityFilters): Promise; + entityByUid(uid: string): Promise; entityByName( kind: string, namespace: string | undefined, name: string, - ): Promise; + ): Promise; }; // diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index a82385aae4..7a38adf02f 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -19,9 +19,9 @@ import { getVoidLogger, NotFoundError, } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; import Knex from 'knex'; import path from 'path'; -import { DescriptorEnvelope } from '../ingestion'; import { Database } from './Database'; import { AddDatabaseLocation, @@ -247,8 +247,8 @@ describe('Database', () => { describe('entities', () => { it('can get all entities with empty filters list', async () => { const catalog = new Database(database, getVoidLogger()); - const e1: DescriptorEnvelope = { apiVersion: 'a', kind: 'b' }; - const e2: DescriptorEnvelope = { + const e1: Entity = { apiVersion: 'a', kind: 'b' }; + const e2: Entity = { apiVersion: 'a', kind: 'b', spec: { c: null }, @@ -271,7 +271,7 @@ describe('Database', () => { it('can get all specific entities for matching filters (naive case)', async () => { const catalog = new Database(database, getVoidLogger()); - const entities: DescriptorEnvelope[] = [ + const entities: Entity[] = [ { apiVersion: 'a', kind: 'b' }, { apiVersion: 'a', @@ -305,7 +305,7 @@ describe('Database', () => { it('can get all specific entities for matching filters with nulls (both missing and literal null value)', async () => { const catalog = new Database(database, getVoidLogger()); - const entities: DescriptorEnvelope[] = [ + const entities: Entity[] = [ { apiVersion: 'a', kind: 'b' }, { apiVersion: 'a', diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index 614f7d09bf..db9e76a0a8 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -19,12 +19,12 @@ import { InputError, NotFoundError, } from '@backstage/backend-common'; +import { Entity, EntityMeta } from '@backstage/catalog-model'; import Knex from 'knex'; import lodash from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from 'winston'; import { EntityFilters } from '../catalog'; -import { DescriptorEnvelope, EntityMeta } from '../ingestion'; import { buildEntitySearch } from './search'; import { AddDatabaseLocation, @@ -54,9 +54,7 @@ function serializeMetadata(metadata: EntityMeta | undefined): string | null { return JSON.stringify(getStrippedMetadata(metadata)); } -function serializeSpec( - spec: DescriptorEnvelope['spec'], -): DbEntitiesRow['spec'] { +function serializeSpec(spec: Entity['spec']): DbEntitiesRow['spec'] { if (!spec) { return null; } @@ -66,7 +64,7 @@ function serializeSpec( function toEntityRow( locationId: string | undefined, - entity: DescriptorEnvelope, + entity: Entity, ): DbEntitiesRow { return { id: entity.metadata!.uid!, @@ -83,7 +81,7 @@ function toEntityRow( } function toEntityResponse(row: DbEntitiesRow): DbEntityResponse { - const entity: DescriptorEnvelope = { + const entity: Entity = { apiVersion: row.api_version, kind: row.kind, metadata: { @@ -94,7 +92,7 @@ function toEntityResponse(row: DbEntitiesRow): DbEntityResponse { }; if (row.metadata) { - const metadata = JSON.parse(row.metadata) as DescriptorEnvelope['metadata']; + const metadata = JSON.parse(row.metadata) as Entity['metadata']; entity.metadata = { ...entity.metadata, ...metadata }; } @@ -422,7 +420,7 @@ export class Database { private async updateEntitiesSearch( tx: Knex.Transaction, entityId: string, - data: DescriptorEnvelope, + data: Entity, ): Promise { try { const entries = buildEntitySearch(entityId, data); diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts index ca50f518cb..c8d0aec332 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.test.ts @@ -15,13 +15,9 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { Entity, EntityPolicy } from '@backstage/catalog-model'; import Knex from 'knex'; -import { - ComponentDescriptor, - DescriptorParser, - LocationReader, - ParserError, -} from '../ingestion'; +import { IngestionModel } from '../ingestion/types'; import { Database } from './Database'; import { DatabaseManager } from './DatabaseManager'; import { DatabaseLocationUpdateLogStatus, DbLocationsRow } from './types'; @@ -32,18 +28,18 @@ describe('DatabaseManager', () => { const db = ({ locations: jest.fn().mockResolvedValue([]), } as unknown) as Database; - const reader: LocationReader = { - read: jest.fn(), + const reader: IngestionModel = { + readLocation: jest.fn(), }; - const parser: DescriptorParser = { - parse: jest.fn(), + const policy: EntityPolicy = { + apply: jest.fn(), }; await expect( - DatabaseManager.refreshLocations(db, reader, parser, getVoidLogger()), + DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); - expect(reader.read).not.toHaveBeenCalled(); - expect(parser.parse).not.toHaveBeenCalled(); + expect(reader.readLocation).not.toHaveBeenCalled(); + expect(policy.apply).not.toHaveBeenCalled(); }); it('can update a single location', async () => { @@ -52,7 +48,7 @@ describe('DatabaseManager', () => { type: 'some', target: 'thing', }; - const desc: ComponentDescriptor = { + const desc: Entity = { apiVersion: 'backstage.io/v1beta1', kind: 'Component', metadata: { name: 'c1' }, @@ -68,18 +64,20 @@ describe('DatabaseManager', () => { addLocationUpdateLogEvent: jest.fn(), } as Partial) as Database; - const reader: LocationReader = { - read: jest.fn(() => Promise.resolve([{ type: 'data', data: desc }])), + const reader: IngestionModel = { + readLocation: jest.fn(() => + Promise.resolve([{ type: 'data', data: desc }]), + ), }; - const parser: DescriptorParser = { - parse: jest.fn(() => Promise.resolve(desc)), + const policy: EntityPolicy = { + apply: jest.fn(() => Promise.resolve(desc)), }; await expect( - DatabaseManager.refreshLocations(db, reader, parser, getVoidLogger()), + DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); - expect(reader.read).toHaveBeenCalledTimes(1); - expect(reader.read).toHaveBeenNthCalledWith(1, 'some', 'thing'); + expect(reader.readLocation).toHaveBeenCalledTimes(1); + expect(reader.readLocation).toHaveBeenNthCalledWith(1, 'some', 'thing'); expect(db.addEntity).toHaveBeenCalledTimes(1); expect(db.addEntity).toHaveBeenNthCalledWith(1, undefined, { locationId: '123', @@ -108,21 +106,23 @@ describe('DatabaseManager', () => { addLocationUpdateLogEvent: jest.fn(), } as unknown) as Database; - const desc: ComponentDescriptor = { + const desc: Entity = { apiVersion: 'backstage.io/v1beta1', kind: 'Component', metadata: { name: 'c1' }, spec: { type: 'service' }, }; - const reader: LocationReader = { - read: jest.fn(() => Promise.resolve([{ type: 'data', data: desc }])), + const reader: IngestionModel = { + readLocation: jest.fn(() => + Promise.resolve([{ type: 'data', data: desc }]), + ), }; - const parser: DescriptorParser = { - parse: jest.fn(() => Promise.resolve(desc)), + const policy: EntityPolicy = { + apply: jest.fn(() => Promise.resolve(desc)), }; await expect( - DatabaseManager.refreshLocations(db, reader, parser, getVoidLogger()), + DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( @@ -158,23 +158,23 @@ describe('DatabaseManager', () => { addLocationUpdateLogEvent: jest.fn(), } as unknown) as Database; - const desc: ComponentDescriptor = { + const desc: Entity = { apiVersion: 'backstage.io/v1beta1', kind: 'Component', metadata: { name: 'c1' }, spec: { type: 'service' }, }; - const reader: LocationReader = { - read: jest.fn(() => Promise.resolve([{ type: 'data', data: desc }])), - }; - const parser: DescriptorParser = { - parse: jest.fn(() => - Promise.reject(new ParserError('parser error message', 'c1')), + const reader: IngestionModel = { + readLocation: jest.fn(() => + Promise.resolve([{ type: 'data', data: desc }]), ), }; + const policy: EntityPolicy = { + apply: jest.fn(() => Promise.reject(new Error('parser error message'))), + }; await expect( - DatabaseManager.refreshLocations(db, reader, parser, getVoidLogger()), + DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( @@ -211,19 +211,17 @@ describe('DatabaseManager', () => { addLocationUpdateLogEvent: jest.fn(), } as unknown) as Database; - const reader: LocationReader = { - read: jest.fn(() => + const reader: IngestionModel = { + readLocation: jest.fn(() => Promise.reject([{ type: 'error', error: new Error('test message') }]), ), }; - const parser: DescriptorParser = { - parse: jest.fn(() => - Promise.reject(new ParserError('parser error message', 'c1')), - ), + const policy: EntityPolicy = { + apply: jest.fn(() => Promise.reject(new Error('parser error message'))), }; await expect( - DatabaseManager.refreshLocations(db, reader, parser, getVoidLogger()), + DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index 7f646971db..dff589c525 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -14,18 +14,14 @@ * limitations under the License. */ +import { Entity, EntityPolicy } from '@backstage/catalog-model'; import Knex from 'knex'; import lodash from 'lodash'; import path from 'path'; import { Logger } from 'winston'; -import { - DescriptorEnvelope, - DescriptorParser, - LocationReader, - ParserError, -} from '../ingestion'; import { Database } from './Database'; import { DatabaseLocationUpdateLogStatus, DbEntityRequest } from './types'; +import { IngestionModel } from '../ingestion/types'; export class DatabaseManager { public static async createDatabase( @@ -67,8 +63,8 @@ export class DatabaseManager { public static async refreshLocations( database: Database, - reader: LocationReader, - parser: DescriptorParser, + ingestionModel: IngestionModel, + entityPolicy: EntityPolicy, logger: Logger, ): Promise { const locations = await database.locations(); @@ -78,7 +74,10 @@ export class DatabaseManager { `Refreshing location id="${location.id}" type="${location.type}" target="${location.target}"`, ); - const readerOutput = await reader.read(location.type, location.target); + const readerOutput = await ingestionModel.readLocation( + location.type, + location.target, + ); for (const readerItem of readerOutput) { if (readerItem.type === 'error') { @@ -87,7 +86,7 @@ export class DatabaseManager { } try { - const entity = await parser.parse(readerItem.data); + const entity = await entityPolicy.apply(readerItem.data); await DatabaseManager.refreshSingleEntity( database, location.id, @@ -100,15 +99,11 @@ export class DatabaseManager { entity.metadata!.name, ); } catch (error) { - let entityName; - if (error instanceof ParserError) { - entityName = error.entityName; - } await DatabaseManager.logUpdateFailure( database, location.id, error, - entityName, + readerItem.data.metadata?.name, ); } } @@ -129,7 +124,7 @@ export class DatabaseManager { private static async refreshSingleEntity( database: Database, locationId: string, - entity: DescriptorEnvelope, + entity: Entity, logger: Logger, ): Promise { const { kind } = entity; @@ -163,10 +158,7 @@ export class DatabaseManager { }); } - private static entitiesAreEqual( - first: DescriptorEnvelope, - second: DescriptorEnvelope, - ) { + private static entitiesAreEqual(first: Entity, second: Entity) { const firstClone = lodash.cloneDeep(first); const secondClone = lodash.cloneDeep(second); diff --git a/plugins/catalog-backend/src/database/migrations/20200520140700_location_update_log_table.ts b/plugins/catalog-backend/src/database/migrations/20200520140700_location_update_log_table.ts index b2e1dc0d32..6700f5748e 100644 --- a/plugins/catalog-backend/src/database/migrations/20200520140700_location_update_log_table.ts +++ b/plugins/catalog-backend/src/database/migrations/20200520140700_location_update_log_table.ts @@ -19,7 +19,10 @@ export async function up(knex: Knex): Promise { 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 + .dateTime('created_at') + .defaultTo(knex.fn.now()) + .notNullable(); table.string('message'); table .uuid('location_id') diff --git a/plugins/catalog-backend/src/database/search.test.ts b/plugins/catalog-backend/src/database/search.test.ts index 26b20d1846..7ad06aee14 100644 --- a/plugins/catalog-backend/src/database/search.test.ts +++ b/plugins/catalog-backend/src/database/search.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DescriptorEnvelope } from '../ingestion'; +import { Entity } from '@backstage/catalog-model'; import { buildEntitySearch, visitEntityPart } from './search'; import { DbEntitiesSearchRow } from './types'; @@ -99,7 +99,7 @@ describe('search', () => { describe('buildEntitySearch', () => { it('adds special keys even if missing', () => { - const input: DescriptorEnvelope = { + const input: Entity = { apiVersion: 'a', kind: 'b', }; @@ -116,7 +116,7 @@ describe('search', () => { }); it('adds prefix-stripped versions', () => { - const input: DescriptorEnvelope = { + const input: Entity = { apiVersion: 'a', kind: 'b', metadata: { diff --git a/plugins/catalog-backend/src/database/search.ts b/plugins/catalog-backend/src/database/search.ts index f54cfde7e6..fcacf1a9f2 100644 --- a/plugins/catalog-backend/src/database/search.ts +++ b/plugins/catalog-backend/src/database/search.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DescriptorEnvelope } from '../ingestion'; +import { Entity } from '@backstage/catalog-model'; import { DbEntitiesSearchRow } from './types'; // Search entries that start with these prefixes, also get a shorthand without @@ -119,7 +119,7 @@ export function visitEntityPart( */ export function buildEntitySearch( entityId: string, - entity: DescriptorEnvelope, + entity: Entity, ): DbEntitiesSearchRow[] { // Start with some special keys that are always present because you want to // be able to easily search for null specifically diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 06e234545f..ca1cbbdfd4 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import * as yup from 'yup'; -import { DescriptorEnvelope } from '../ingestion'; export type DbEntitiesRow = { id: string; @@ -32,12 +32,12 @@ export type DbEntitiesRow = { export type DbEntityRequest = { locationId?: string; - entity: DescriptorEnvelope; + entity: Entity; }; export type DbEntityResponse = { locationId?: string; - entity: DescriptorEnvelope; + entity: Entity; }; export type DbEntitiesSearchRow = { diff --git a/plugins/catalog-backend/src/ingestion/DescriptorParsers.ts b/plugins/catalog-backend/src/ingestion/DescriptorParsers.ts deleted file mode 100644 index 86283df666..0000000000 --- a/plugins/catalog-backend/src/ingestion/DescriptorParsers.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { makeValidator } from '../validation'; -import { ComponentDescriptorV1beta1Parser } from './descriptors/ComponentDescriptorV1beta1Parser'; -import { DescriptorEnvelopeParser } from './descriptors/DescriptorEnvelopeParser'; -import { - DescriptorEnvelope, - DescriptorParser, - KindParser, - ParserError, -} from './types'; - -export class DescriptorParsers implements DescriptorParser { - static create(): DescriptorParser { - const validators = makeValidator(); - return new DescriptorParsers(new DescriptorEnvelopeParser(validators), [ - new ComponentDescriptorV1beta1Parser(), - ]); - } - - constructor( - private readonly envelopeParser: DescriptorEnvelopeParser, - private readonly kindParsers: KindParser[], - ) {} - - async parse(descriptor: object): Promise { - const envelope = await this.envelopeParser.parse(descriptor); - for (const parser of this.kindParsers) { - const parsed = await parser.tryParse(envelope); - if (parsed) { - return parsed; - } - } - throw new ParserError( - `Unsupported object ${envelope.apiVersion}, ${envelope.kind}`, - envelope.metadata?.name, - ); - } -} diff --git a/plugins/catalog-backend/src/ingestion/IngestionModels.ts b/plugins/catalog-backend/src/ingestion/IngestionModels.ts new file mode 100644 index 0000000000..616f1b49ae --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/IngestionModels.ts @@ -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. + */ + +import { EntityPolicy, EntityPolicies } from '@backstage/catalog-model'; +import { DescriptorParser, ReaderOutput } from './descriptor/parsers/types'; +import { LocationReader, LocationReaders } from './source'; +import { IngestionModel } from './types'; +import { DescriptorParsers } from './descriptor'; + +export class IngestionModels implements IngestionModel { + private readonly reader: LocationReader; + private readonly parser: DescriptorParser; + private readonly entityPolicy: EntityPolicy; + + static default(): IngestionModel { + return new IngestionModels( + new LocationReaders(), + new DescriptorParsers(), + new EntityPolicies(), + ); + } + + constructor( + reader: LocationReader, + parser: DescriptorParser, + entityPolicy: EntityPolicy, + ) { + this.reader = reader; + this.parser = parser; + this.entityPolicy = entityPolicy; + } + + async readLocation(type: string, target: string): Promise { + const buffer = await this.reader.tryRead(type, target); + if (!buffer) { + throw new Error(`No reader could handle location ${type} ${target}`); + } + + const items = await this.parser.tryParse(buffer); + if (!items) { + throw new Error(`No parser could handle location ${type} ${target}`); + } + + const result: ReaderOutput[] = []; + for (const item of items) { + if (item.type === 'error') { + result.push(item); + } else { + try { + const output = await this.entityPolicy.apply(item.data); + result.push({ type: 'data', data: output }); + } catch (e) { + result.push({ type: 'error', error: e }); + } + } + } + + return result; + } +} diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts deleted file mode 100644 index aa88d299e4..0000000000 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FileLocationSource } from './sources/FileLocationSource'; -import { GitHubLocationSource } from './sources/GitHubLocationSource'; -import { LocationReader, LocationSource, ReaderOutput } from './types'; - -export class LocationReaders implements LocationReader { - static create(): LocationReader { - return new LocationReaders({ - file: new FileLocationSource(), - github: new GitHubLocationSource(), - }); - } - - constructor(private readonly sources: Record) {} - - async read(type: string, target: string): Promise { - const source = this.sources[type]; - if (!source) { - throw new Error(`Unknown location type ${type}`); - } - - return source.read(target); - } -} diff --git a/plugins/catalog-backend/src/ingestion/descriptor/DescriptorParsers.ts b/plugins/catalog-backend/src/ingestion/descriptor/DescriptorParsers.ts new file mode 100644 index 0000000000..ed05855109 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/descriptor/DescriptorParsers.ts @@ -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. + */ + +import { DescriptorParser, ReaderOutput } from './parsers/types'; +import { YamlDescriptorParser } from './parsers/YamlDescriptorParser'; + +/** + * Parses raw descriptor data (e.g. from a file or stream) into entities. + */ +export class DescriptorParsers implements DescriptorParser { + private readonly parsers: DescriptorParser[]; + + static defaultParsers(): DescriptorParser[] { + return [new YamlDescriptorParser()]; + } + + constructor( + parsers: DescriptorParser[] = DescriptorParsers.defaultParsers(), + ) { + this.parsers = parsers; + } + + async tryParse(data: Buffer): Promise { + for (const parser of this.parsers) { + const result = await parser.tryParse(data); + if (result) { + return result; + } + } + throw new Error(`Unsupported descriptor format`); + } +} diff --git a/plugins/catalog-backend/src/ingestion/descriptor/index.ts b/plugins/catalog-backend/src/ingestion/descriptor/index.ts new file mode 100644 index 0000000000..1529c78afc --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/descriptor/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { DescriptorParsers } from './DescriptorParsers'; +export { YamlDescriptorParser } from './parsers/YamlDescriptorParser'; diff --git a/plugins/catalog-backend/src/ingestion/descriptor/parsers/YamlDescriptorParser.ts b/plugins/catalog-backend/src/ingestion/descriptor/parsers/YamlDescriptorParser.ts new file mode 100644 index 0000000000..3f2e9e3c5c --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/descriptor/parsers/YamlDescriptorParser.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import yaml from 'yaml'; +import { DescriptorParser, ReaderOutput } from './types'; + +/** + * Parses descriptors on YAML format + */ +export class YamlDescriptorParser implements DescriptorParser { + async tryParse(data: Buffer): Promise { + // TODO(freben): Should perhaps first do format detection, so the parse + // failure can be emitted as a proper error instead of just as if we + // weren't handling the format at all. + let documents; + try { + documents = yaml.parseAllDocuments(data.toString('utf8')); + } catch (e) { + return undefined; + } + + const result: ReaderOutput[] = []; + + for (const document of documents) { + if (document.contents) { + if (document.errors?.length) { + result.push({ + type: 'error', + error: new Error(`Malformed YAML document, ${document.errors[0]}`), + }); + } else { + const json = document.toJSON(); + if (typeof json !== 'object' || Array.isArray(json)) { + result.push({ + type: 'error', + error: new Error(`Malformed descriptor, expected object at root`), + }); + } else { + result.push({ + type: 'data', + data: json as Entity, + }); + } + } + } + } + + return result; + } +} diff --git a/plugins/catalog-backend/src/ingestion/descriptor/parsers/types.ts b/plugins/catalog-backend/src/ingestion/descriptor/parsers/types.ts new file mode 100644 index 0000000000..baef5dc3df --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/descriptor/parsers/types.ts @@ -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. + */ + +import { Entity } from '@backstage/catalog-model'; + +export type ReaderOutput = + | { type: 'error'; error: Error } + | { type: 'data'; data: Entity }; + +/** + * Parses raw descriptor data (e.g. from a file) into entities. + */ +export type DescriptorParser = { + /** + * Try to parse some raw data into an entity. + * + * Note that this is only the low level operation of parsing the raw file + * format, e.g. reading JSON or YAML or similar and emitting as structured + * but unvalidated data. The actual validation is performed by EntityPolicy + * and KindParser. + * + * @param data Raw descriptor data + * @returns A list of raw unvalidated entities / errors, or undefined if the + * given data is not meant to be handled by this parser + * @throws An Error if the format was handled and found to not be properly + * formed + */ + tryParse(data: Buffer): Promise; +}; diff --git a/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.test.ts b/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.test.ts deleted file mode 100644 index 7c96fb7cf5..0000000000 --- a/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import yaml from 'yaml'; -import { makeValidator } from '../../validation'; -import { DescriptorEnvelopeParser } from './DescriptorEnvelopeParser'; - -describe('DescriptorEnvelopeParser', () => { - let data: any; - let parser: DescriptorEnvelopeParser; - - beforeEach(() => { - data = yaml.parse(` - apiVersion: backstage.io/v1beta1 - kind: Component - metadata: - uid: e01199ab-08cc-44c2-8e19-5c29ded82521 - etag: lsndfkjsndfkjnsdfkjnsd== - generation: 13 - name: my-component-yay - namespace: the-namespace - labels: - backstage.io/custom: ValueStuff - annotations: - example.com/bindings: are-secret - spec: - custom: stuff - `); - parser = new DescriptorEnvelopeParser(makeValidator()); - }); - - it('works for the happy path', async () => { - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects missing apiVersion', async () => { - delete data.apiVersion; - await expect(parser.parse(data)).rejects.toThrow(/apiVersion/); - }); - - it('rejects wrong root type', async () => { - await expect(parser.parse(7)).rejects.toThrow(/object/); - }); - - it('rejects bad apiVersion', async () => { - data.apiVersion = 'a#b'; - await expect(parser.parse(data)).rejects.toThrow(/apiVersion/); - }); - - it('rejects missing kind', async () => { - delete data.kind; - await expect(parser.parse(data)).rejects.toThrow(/kind/); - }); - - it('rejects bad kind', async () => { - data.kind = 'a#b'; - await expect(parser.parse(data)).rejects.toThrow(/kind/); - }); - - it('accepts missing metadata', async () => { - delete data.medatata; - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects non-object metadata', async () => { - data.metadata = 7; - await expect(parser.parse(data)).rejects.toThrow(/metadata/); - }); - - it('accepts missing uid', async () => { - delete data.metadata.uid; - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects bad uid', async () => { - data.metadata.uid = 7; - await expect(parser.parse(data)).rejects.toThrow(/uid/); - }); - - it('accepts missing etag', async () => { - delete data.metadata.etag; - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects bad etag', async () => { - data.metadata.etag = 7; - await expect(parser.parse(data)).rejects.toThrow(/etag/); - }); - - it('accepts missing generation', async () => { - delete data.metadata.generation; - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects bad generation', async () => { - data.metadata.generation = 'a'; - await expect(parser.parse(data)).rejects.toThrow(/generation/); - }); - - it('accepts missing spec', async () => { - delete data.spec; - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects non-object spec', async () => { - data.spec = 7; - await expect(parser.parse(data)).rejects.toThrow(/spec/); - }); - - it('rejects bad name', async () => { - data.metadata.name = 7; - await expect(parser.parse(data)).rejects.toThrow(/name/); - }); - - it('rejects bad namespace', async () => { - data.metadata.namespace = 7; - await expect(parser.parse(data)).rejects.toThrow(/namespace/); - }); - - it('rejects bad label key', async () => { - data.metadata.labels['a#b'] = 'value'; - await expect(parser.parse(data)).rejects.toThrow(/label.*key/i); - }); - - it('rejects bad label value', async () => { - data.metadata.labels.a = 'a#b'; - await expect(parser.parse(data)).rejects.toThrow(/label.*value/i); - }); - - it('rejects bad annotation key', async () => { - data.metadata.annotations['a#b'] = 'value'; - await expect(parser.parse(data)).rejects.toThrow(/annotation.*key/i); - }); - - it('rejects bad annotation value', async () => { - data.metadata.annotations.a = []; - await expect(parser.parse(data)).rejects.toThrow(/annotation.*value/i); - }); - - it('rejects unknown root keys', async () => { - data.spec2 = {}; - await expect(parser.parse(data)).rejects.toThrow(/spec2/i); - }); - - it('rejects reserved keys in the spec root', async () => { - data.spec.apiVersion = 'a/b'; - await expect(parser.parse(data)).rejects.toThrow(/spec.*apiVersion/i); - }); - - it('rejects reserved keys in labels', async () => { - data.metadata.labels.apiVersion = 'a'; - await expect(parser.parse(data)).rejects.toThrow(/label.*apiVersion/i); - }); - - it('rejects reserved keys in annotations', async () => { - data.metadata.annotations.apiVersion = 'a'; - await expect(parser.parse(data)).rejects.toThrow(/annotation.*apiVersion/i); - }); -}); diff --git a/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts b/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts deleted file mode 100644 index 1f21e2df9e..0000000000 --- a/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as yup from 'yup'; -import { Validators } from '../../validation'; -import { DescriptorEnvelope } from '../types'; - -/** - * Parses some raw structured data as a descriptor envelope - */ -export class DescriptorEnvelopeParser { - private schema: yup.Schema; - - constructor(validators: Validators) { - const apiVersionSchema = yup - .string() - .required() - .test( - 'apiVersion', - 'The apiVersion is not formatted according to schema', - validators.isValidApiVersion, - ); - - const kindSchema = yup - .string() - .required() - .test( - 'kind', - 'The kind is not formatted according to schema', - validators.isValidKind, - ); - - const uidSchema = yup - .string() - .notRequired() - .test( - 'metadata.uid', - 'The uid is not formatted according to schema', - value => value === undefined || value.length > 0, - ); - - const etagSchema = yup - .string() - .notRequired() - .test( - 'metadata.etag', - 'The etag value is not according to schema', - value => value === undefined || value.length > 0, - ); - - const generationSchema = yup - .number() - .notRequired() - .test( - 'metadata.generation', - 'The generation value is not according to schema', - value => value === undefined || value > 0, - ); - - const nameSchema = yup - .string() - .notRequired() - .test( - 'metadata.name', - 'The name is not formatted according to schema', - value => value === undefined || validators.isValidEntityName(value), - ); - - const namespaceSchema = yup - .string() - .notRequired() - .test( - 'metadata.namespace', - 'The namespace is malformed', - value => value === undefined || validators.isValidNamespace(value), - ); - - const labelsSchema = yup - .object>() - .notRequired() - .test({ - name: 'metadata.labels.keys', - message: 'Label keys not formatted according to schema', - test(value: object) { - return ( - value === undefined || - Object.keys(value).every(validators.isValidLabelKey) - ); - }, - }) - .test({ - name: 'metadata.labels.values', - message: 'Label values not formatted according to schema', - test(value: object) { - return ( - value === undefined || - Object.values(value).every(validators.isValidLabelValue) - ); - }, - }); - - const annotationsSchema = yup - .object>() - .notRequired() - .test({ - name: 'metadata.annotations.keys', - message: 'Annotation keys not formatted according to schema', - test(value: object) { - return ( - value === undefined || - Object.keys(value).every(validators.isValidAnnotationKey) - ); - }, - }) - .test({ - name: 'metadata.annotations.values', - message: 'Annotation values not formatted according to schema', - test(value: object) { - return ( - value === undefined || - Object.values(value).every(validators.isValidAnnotationValue) - ); - }, - }); - - const metadataSchema = yup - .object({ - uid: uidSchema, - etag: etagSchema, - generation: generationSchema, - name: nameSchema, - namespace: namespaceSchema, - labels: labelsSchema, - annotations: annotationsSchema, - }) - .notRequired(); - - const specSchema = yup.object({}).notRequired(); - - this.schema = yup - .object({ - apiVersion: apiVersionSchema, - kind: kindSchema, - metadata: metadataSchema, - spec: specSchema, - }) - .noUnknown(); - } - - async parse(data: any): Promise { - let result: DescriptorEnvelope; - try { - result = await this.schema.validate(data, { strict: true }); - } catch (e) { - throw new Error(`Malformed envelope, ${e}`); - } - - // These are keys with specific semantic meaning in a document, that we do - // not want to appear in the root of the spec, or as labels or as - // annotations, because they will lead to confusion. - const reservedKeys = [ - 'apiVersion', - 'kind', - 'uid', - 'etag', - 'generation', - 'name', - 'namespace', - 'labels', - 'annotations', - 'spec', - ]; - for (const key of reservedKeys) { - if (result.spec?.hasOwnProperty(key)) { - throw new Error( - `The spec may not contain the key ${key}, because it has reserved meaning`, - ); - } - if (result.metadata?.labels?.hasOwnProperty(key)) { - throw new Error( - `A label may not have the key ${key}, because it has reserved meaning`, - ); - } - if (result.metadata?.annotations?.hasOwnProperty(key)) { - throw new Error( - `An annotation may not have the key ${key}, because it has reserved meaning`, - ); - } - } - - return result; - } -} diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index ca6f2dd2be..b6aceaecdd 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ -export * from './DescriptorParsers'; -export * from './LocationReaders'; -export * from './types'; +export * from './descriptor'; +export { IngestionModels } from './IngestionModels'; +export * from './source'; +export type { IngestionModel } from './types'; diff --git a/plugins/catalog-backend/src/ingestion/source/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/source/LocationReaders.ts new file mode 100644 index 0000000000..a670f309ad --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/source/LocationReaders.ts @@ -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. + */ + +import { FileLocationReader } from './readers/FileLocationReader'; +import { GitHubLocationReader } from './readers/GitHubLocationReader'; +import { LocationReader } from './readers/types'; + +export class LocationReaders implements LocationReader { + private readonly readers: LocationReader[]; + + static defaultReaders(): LocationReader[] { + return [new FileLocationReader(), new GitHubLocationReader()]; + } + + constructor(readers: LocationReader[] = LocationReaders.defaultReaders()) { + this.readers = readers; + } + + async tryRead(type: string, target: string): Promise { + for (const reader of this.readers) { + const result = await reader.tryRead(type, target); + if (result) { + return result; + } + } + throw new Error(`Could not read unknown location "${type}", "${target}"`); + } +} diff --git a/plugins/catalog-backend/src/ingestion/source/index.ts b/plugins/catalog-backend/src/ingestion/source/index.ts new file mode 100644 index 0000000000..3ed1063878 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/source/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { LocationReaders } from './LocationReaders'; +export { FileLocationReader } from './readers/FileLocationReader'; +export { GitHubLocationReader } from './readers/GitHubLocationReader'; +export { LocationReader } from './readers/types'; diff --git a/plugins/catalog-backend/src/ingestion/sources/FileLocationSource.ts b/plugins/catalog-backend/src/ingestion/source/readers/FileLocationReader.ts similarity index 63% rename from plugins/catalog-backend/src/ingestion/sources/FileLocationSource.ts rename to plugins/catalog-backend/src/ingestion/source/readers/FileLocationReader.ts index 9d2794ec4f..0c64aebf14 100644 --- a/plugins/catalog-backend/src/ingestion/sources/FileLocationSource.ts +++ b/plugins/catalog-backend/src/ingestion/source/readers/FileLocationReader.ts @@ -15,22 +15,21 @@ */ import fs from 'fs-extra'; -import { LocationSource, ReaderOutput } from '../types'; -import { readDescriptorYaml } from './util'; +import { LocationReader } from './types'; + +/** + * Reads a file from the local file system. + */ +export class FileLocationReader implements LocationReader { + async tryRead(type: string, target: string): Promise { + if (type !== 'file') { + return undefined; + } -export class FileLocationSource implements LocationSource { - async read(target: string): Promise { - let rawYaml; try { - rawYaml = await fs.readFile(target, 'utf8'); + return await fs.readFile(target); } catch (e) { throw new Error(`Unable to read "${target}", ${e}`); } - - try { - return readDescriptorYaml(rawYaml); - } catch (e) { - throw new Error(`Malformed descriptor at "${target}", ${e}`); - } } } diff --git a/plugins/catalog-backend/src/ingestion/sources/__tests__/GitHubLocationSource.test.ts b/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts similarity index 64% rename from plugins/catalog-backend/src/ingestion/sources/__tests__/GitHubLocationSource.test.ts rename to plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts index 083c2f7e86..1f0f6ed539 100644 --- a/plugins/catalog-backend/src/ingestion/sources/__tests__/GitHubLocationSource.test.ts +++ b/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts @@ -16,43 +16,26 @@ jest.mock('node-fetch'); -import fs from 'fs-extra'; import fetch from 'node-fetch'; -import path from 'path'; -import { GitHubLocationSource } from '../GitHubLocationSource'; +import { GitHubLocationReader } from './GitHubLocationReader'; const { Response } = jest.requireActual('node-fetch'); -const FIXTURES_DIR = path.resolve( - __dirname, - '..', - '..', - '..', - '..', - 'fixtures', -); -const fixtures = fs.readdirSync(FIXTURES_DIR).reduce((acc, filename) => { - acc[filename] = fs.readFileSync(path.resolve(FIXTURES_DIR, filename), 'utf8'); - return acc; -}, {} as Record); - -describe('Unit: GitHubLocationSource', () => { +describe('Unit: GitHubLocationReader', () => { beforeEach(() => { jest.clearAllMocks(); }); it('fetches the file and parses it correctly', async () => { - (fetch as any).mockReturnValueOnce( - Promise.resolve(new Response(fixtures['one_component.yaml'])), - ); - const reader = new GitHubLocationSource(); + (fetch as any).mockResolvedValueOnce(new Response('hello')); - const result = await reader.read( + const reader = new GitHubLocationReader(); + const buffer = await reader.tryRead( + 'github', 'https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/fixtures/one_component.yaml', ); - expect(result[0].type).toBe('data'); - expect((result[0] as any).data.metadata.name).toBe('component3'); + expect(buffer?.toString('utf8')).toBe('hello'); }); it('changes the url to point to https://raw.githubusercontent.com', async () => { @@ -61,12 +44,12 @@ describe('Unit: GitHubLocationSource', () => { const folderPath = `master/plugins/catalog-backend/fixtures`; const componentFilename = `one_component.yaml`; const rawGitHubUrl = `https://raw.githubusercontent.com`; - const reader = new GitHubLocationSource(); - (fetch as any).mockReturnValueOnce( - Promise.resolve(new Response(fixtures[componentFilename])), - ); - await reader.read( + const reader = new GitHubLocationReader(); + (fetch as any).mockResolvedValueOnce(new Response('hello')); + + await reader.tryRead( + 'github', `${gitHubUrl}/${project}/blob/${folderPath}/${componentFilename}`, ); @@ -76,7 +59,7 @@ describe('Unit: GitHubLocationSource', () => { }); describe('rejects wrong urls', () => { - const reader = new GitHubLocationSource(); + const reader = new GitHubLocationReader(); it.each([ ['http://example.com/one_component.yaml'], @@ -87,7 +70,7 @@ describe('Unit: GitHubLocationSource', () => { ])( '%p', async (url: string) => - await expect(reader.read(url)).rejects.toThrow(/url/), + await expect(reader.tryRead('github', url)).rejects.toThrow(/url/), ); }); }); @@ -100,11 +83,10 @@ describe('Integration: GitHubLocationSource', () => { it('fetches the fixture from backstage repo', async () => { const PERMANENT_LINK = 'https://github.com/spotify/backstage/blob/ee84a874f8e37f87940cbe515a86c07a2db29541/plugins/catalog-backend/fixtures/one_component.yaml'; - const reader = new GitHubLocationSource(); - const result = await reader.read(PERMANENT_LINK); + const reader = new GitHubLocationReader(); + const result = await reader.tryRead('github', PERMANENT_LINK); - expect(result[0].type).toBe('data'); - expect((result[0] as any).data.metadata.name).toBe('component3'); + expect(result?.toString('utf8')).toContain('component3'); }); }); diff --git a/plugins/catalog-backend/src/ingestion/sources/GitHubLocationSource.ts b/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.ts similarity index 70% rename from plugins/catalog-backend/src/ingestion/sources/GitHubLocationSource.ts rename to plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.ts index 69ba4911a6..bd330e28b4 100644 --- a/plugins/catalog-backend/src/ingestion/sources/GitHubLocationSource.ts +++ b/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.ts @@ -16,17 +16,31 @@ import fetch from 'node-fetch'; import { URL } from 'url'; -import { LocationSource, ReaderOutput } from '../types'; -import { readDescriptorYaml } from './util'; +import { LocationReader } from './types'; -// Pointing to raw.githubusercontent.com for now -// to be changed in the future, after auth and tokens are done -export class GitHubLocationSource implements LocationSource { - async read(target: string): Promise { - let url: URL; +/** + * Reads a file whose target is a GitHub URL. + * + * Uses raw.githubusercontent.com for now, but this will probably change in the + * future when token auth is implemented. + */ +export class GitHubLocationReader implements LocationReader { + async tryRead(type: string, target: string): Promise { + if (type !== 'github') { + return undefined; + } + const url = this.buildRawUrl(target); try { - url = new URL(target); + return await fetch(url.toString()).then(x => x.buffer()); + } catch (e) { + throw new Error(`Unable to read "${target}", ${e}`); + } + } + + private buildRawUrl(target: string): URL { + try { + const url = new URL(target); const [ empty, @@ -51,23 +65,10 @@ export class GitHubLocationSource implements LocationSource { url.pathname = [empty, userOrOrg, repoName, ...restOfPath].join('/'); url.hostname = 'raw.githubusercontent.com'; url.protocol = 'https'; + + return url; } catch (e) { throw new Error(`Incorrect url: ${target}, ${e}`); } - - let rawYaml; - try { - rawYaml = await fetch(url.toString()).then(x => { - return x.text(); - }); - } catch (e) { - throw new Error(`Unable to read "${target}", ${e}`); - } - - try { - return readDescriptorYaml(rawYaml); - } catch (e) { - throw new Error(`Malformed descriptor at "${target}", ${e}`); - } } } diff --git a/plugins/catalog-backend/src/ingestion/source/readers/types.ts b/plugins/catalog-backend/src/ingestion/source/readers/types.ts new file mode 100644 index 0000000000..37c7b46885 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/source/readers/types.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +export type LocationReader = { + /** + * Reads the contents of a single location. + * + * @param type The type of location to read + * @param target The location target (type-specific) + * @returns The target contents, as a raw Buffer, or undefined if this type + * was not meant to be consumed by this reader + * @throws An error if the type was meant for this reader, but could not be + * read + */ + tryRead(type: string, target: string): Promise; +}; diff --git a/plugins/catalog-backend/src/ingestion/sources/util.ts b/plugins/catalog-backend/src/ingestion/sources/util.ts deleted file mode 100644 index cccbeb92b3..0000000000 --- a/plugins/catalog-backend/src/ingestion/sources/util.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import yaml from 'yaml'; -import { ReaderOutput } from '../types'; - -export function readDescriptorYaml(data: string): ReaderOutput[] { - let documents; - try { - documents = yaml.parseAllDocuments(data); - } catch (e) { - throw new Error(`Could not parse YAML data, ${e}`); - } - - const result: ReaderOutput[] = []; - - for (const document of documents) { - if (document.contents) { - if (document.errors?.length) { - result.push({ - type: 'error', - error: new Error(`Malformed YAML document, ${document.errors[0]}`), - }); - } else { - const json = document.toJSON(); - if (typeof json !== 'object' || Array.isArray(json)) { - result.push({ - type: 'error', - error: new Error(`Malformed descriptor, expected object at root`), - }); - } else { - result.push({ - type: 'data', - data: json, - }); - } - } - } - } - - return result; -} diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index e5083a65f1..8878c2af5b 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -14,172 +14,8 @@ * limitations under the License. */ -import { ComponentDescriptorV1beta1 } from './descriptors/ComponentDescriptorV1beta1Parser'; +import { ReaderOutput } from './descriptor/parsers/types'; -export type ComponentDescriptor = ComponentDescriptorV1beta1; - -/** - * Metadata fields common to all versions/kinds of entity. - * - * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta - */ -export type EntityMeta = { - /** - * A globally unique ID for the entity. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. The field can (optionally) be specified when performing - * update or delete operations, but the server is free to reject requests - * that do so in such a way that it breaks semantics. - */ - uid?: string; - - /** - * An opaque string that changes for each update operation to any part of - * the entity, including metadata. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. The field can (optionally) be specified when performing - * update or delete operations, and the server will then reject the - * operation if it does not match the current stored value. - */ - etag?: string; - - /** - * A positive nonzero number that indicates the current generation of data - * for this entity; the value is incremented each time the spec changes. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. - */ - generation?: number; - - /** - * The name of the entity. - * - * Must be uniqe within the catalog at any given point in time, for any - * given namespace, for any given kind. - */ - name?: string; - - /** - * The namespace that the entity belongs to. - */ - namespace?: string; - - /** - * Key/value pairs of identifying information attached to the entity. - */ - labels?: Record; - - /** - * Key/value pairs of non-identifying auxiliary information attached to the - * entity. - */ - annotations?: Record; -}; - -/** - * The format envelope that's common to all versions/kinds. - * - * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ - */ -export type DescriptorEnvelope = { - /** - * The version of specification format for this particular entity that - * this is written against. - */ - apiVersion: string; - - /** - * The high level entity type being described. - */ - kind: string; - - /** - * Optional metadata related to the entity. - */ - metadata?: EntityMeta; - - /** - * The specification data describing the entity itself. - */ - spec?: object; -}; - -/** - * Parses and validates descriptors. - * - * The output must be validated and well formed. - */ -export type DescriptorParser = { - /** - * Parses and validates a single raw descriptor. - * - * @param descriptor A raw descriptor object - * @returns A structure describing the parsed and validated descriptor - * @throws An Error if the descriptor was malformed - */ - parse(descriptor: object): Promise; -}; - -/** - * Parses and validates a single envelope into its materialized kind. - * - * These parsers may assume that the envelope is already validated and well - * formed. - */ -export type KindParser = { - /** - * Try to parse an envelope into a materialized kind. - * - * @param envelope A valid descriptor envelope - * @returns A materialized type, or undefined if the given version/kind is - * not meant to be handled by this parser - * @throws An Error if the type was handled and found to not be properly - * formatted - */ - tryParse( - envelope: DescriptorEnvelope, - ): Promise; -}; - -export class ParserError extends Error { - constructor(message?: string, private _entityName?: string | undefined) { - super(message); - } - get entityName() { - return this._entityName; - } -} - -export type ReaderOutput = - | { type: 'error'; error: Error } - | { type: 'data'; data: object }; - -export type LocationReader = { - /** - * Reads the contents of a single location. - * - * @param type The type of location to read - * @param target The location target (type-specific) - * @returns The parsed contents, as an array of unverified descriptors or - * errors where the individual documents could not be parsed. - * @throws An error if the location as a whole could not be read - */ - read(type: string, target: string): Promise; -}; - -export type LocationSource = { - /** - * Reads the contents of a single location. - * - * @param target The location target to read - * @returns The parsed contents, as an array of unverified descriptors - * @throws An error if the location target could not be read - */ - read(target: string): Promise; +export type IngestionModel = { + readLocation(type: string, target: string): Promise; }; diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 8c29362015..e405e296b1 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -15,10 +15,10 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; import request from 'supertest'; import { EntitiesCatalog, Location, LocationsCatalog } from '../catalog'; -import { DescriptorEnvelope } from '../ingestion'; import { createRouter } from './router'; class MockEntitiesCatalog implements EntitiesCatalog { @@ -37,7 +37,7 @@ class MockLocationsCatalog implements LocationsCatalog { describe('createRouter', () => { describe('entities', () => { it('happy path: lists entities', async () => { - const entities: DescriptorEnvelope[] = [{ apiVersion: 'a', kind: 'b' }]; + const entities: Entity[] = [{ apiVersion: 'a', kind: 'b' }]; const catalog = new MockEntitiesCatalog(); catalog.entities.mockResolvedValueOnce(entities); @@ -76,7 +76,7 @@ describe('createRouter', () => { describe('entityByUid', () => { it('can fetch entity by uid', async () => { - const entity: DescriptorEnvelope = { + const entity: Entity = { apiVersion: 'a', kind: 'b', metadata: { @@ -117,7 +117,7 @@ describe('createRouter', () => { describe('entityByName', () => { it('can fetch entity by name', async () => { - const entity: DescriptorEnvelope = { + const entity: Entity = { apiVersion: 'a', kind: 'b', metadata: { diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index c3318a7377..0678cd3aef 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -22,7 +22,7 @@ import { addLocationSchema, EntitiesCatalog, EntityFilters, - LocationsCatalog, + LocationsCatalog } from '../catalog'; import { validateRequestBody } from './util'; diff --git a/yarn.lock b/yarn.lock index 6390797c12..74b9e451b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17169,11 +17169,6 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-addons-text-content@0.0.4: - version "0.0.4" - resolved "https://registry.npmjs.org/react-addons-text-content/-/react-addons-text-content-0.0.4.tgz#d2e259fdc951d1d8906c08902002108dce8792e5" - integrity sha1-0uJZ/clR0diQbAiQIAIQjc6HkuU= - react-beautiful-dnd@11.0.3: version "11.0.3" resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz#5678bb3e725d8b56cb7cf57f56e952105fc4f2af" From cdd1d89a43d479839af44f82c6fd4010fee6b654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 15:46:55 +0200 Subject: [PATCH 13/97] Rename DescriptorEnvelope to Entity --- .../src/catalog/DatabaseEntitiesCatalog.ts | 8 +++---- .../catalog/DatabaseLocationsCatalog.test.ts | 5 ++--- .../src/catalog/DatabaseLocationsCatalog.ts | 2 +- .../src/catalog/StaticEntitiesCatalog.ts | 12 +++++----- plugins/catalog-backend/src/catalog/types.ts | 8 +++---- .../src/database/Database.test.ts | 10 ++++----- .../catalog-backend/src/database/Database.ts | 22 ++++++++++--------- .../src/database/DatabaseManager.ts | 9 +++----- .../src/database/search.test.ts | 6 ++--- .../catalog-backend/src/database/search.ts | 4 ++-- plugins/catalog-backend/src/database/types.ts | 6 ++--- .../src/ingestion/DescriptorParsers.ts | 9 ++------ .../ComponentDescriptorV1beta1Parser.ts | 8 +++---- .../descriptors/DescriptorEnvelopeParser.ts | 8 +++---- .../catalog-backend/src/ingestion/types.ts | 8 +++---- .../src/service/router.test.ts | 12 +++++----- 16 files changed, 64 insertions(+), 73 deletions(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 9d0d8fcaf7..d14b9df658 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -15,20 +15,20 @@ */ import { Database } from '../database'; -import { DescriptorEnvelope } from '../ingestion/types'; +import { Entity } from '../ingestion/types'; import { EntitiesCatalog, EntityFilters } from './types'; export class DatabaseEntitiesCatalog implements EntitiesCatalog { constructor(private readonly database: Database) {} - async entities(filters?: EntityFilters): Promise { + async entities(filters?: EntityFilters): Promise { const items = await this.database.transaction(tx => this.database.entities(tx, filters), ); return items.map(i => i.entity); } - async entityByUid(uid: string): Promise { + async entityByUid(uid: string): Promise { const matches = await this.database.transaction(tx => this.database.entities(tx, [{ key: 'uid', values: [uid] }]), ); @@ -40,7 +40,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { kind: string, name: string, namespace: string | undefined, - ): Promise { + ): Promise { const matches = await this.database.transaction(tx => this.database.entities(tx, [ { key: 'kind', values: [kind] }, diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts index 0181b7fc28..a63dd7d497 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; +import { getVoidLogger } from '@backstage/backend-common'; import knex from 'knex'; import path from 'path'; - import { Database } from '../database'; import { ReaderOutput } from '../ingestion/types'; -import { getVoidLogger } from '@backstage/backend-common'; +import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; describe('DatabaseLocationsCatalog', () => { const database = knex({ diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index 6e3c9ae306..d82d8e2c26 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -15,8 +15,8 @@ */ import { Database } from '../database'; -import { AddLocation, Location, LocationsCatalog } from './types'; import { LocationReader } from '../ingestion'; +import { AddLocation, Location, LocationsCatalog } from './types'; export class DatabaseLocationsCatalog implements LocationsCatalog { constructor( diff --git a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts index 371cdf1f76..64cee69dd7 100644 --- a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts @@ -16,21 +16,21 @@ import { NotFoundError } from '@backstage/backend-common'; import lodash from 'lodash'; -import { DescriptorEnvelope } from '../ingestion'; +import { Entity } from '../ingestion'; import { EntitiesCatalog } from './types'; export class StaticEntitiesCatalog implements EntitiesCatalog { - private _entities: DescriptorEnvelope[]; + private _entities: Entity[]; - constructor(entities: DescriptorEnvelope[]) { + constructor(entities: Entity[]) { this._entities = entities; } - async entities(): Promise { + async entities(): Promise { return lodash.cloneDeep(this._entities); } - async entityByUid(uid: string): Promise { + async entityByUid(uid: string): Promise { const item = this._entities.find(e => uid === e.metadata?.uid); if (!item) { throw new NotFoundError('Entity cannot be found'); @@ -42,7 +42,7 @@ export class StaticEntitiesCatalog implements EntitiesCatalog { kind: string, name: string, namespace: string | undefined, - ): Promise { + ): Promise { const item = this._entities.find( e => kind === e.kind && diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index d5627ea86a..90bb6943b3 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -15,7 +15,7 @@ */ import * as yup from 'yup'; -import { DescriptorEnvelope } from '../ingestion'; +import { Entity } from '../ingestion'; // // Entities @@ -28,13 +28,13 @@ export type EntityFilter = { export type EntityFilters = EntityFilter[]; export type EntitiesCatalog = { - entities(filters?: EntityFilters): Promise; - entityByUid(uid: string): Promise; + entities(filters?: EntityFilters): Promise; + entityByUid(uid: string): Promise; entityByName( kind: string, namespace: string | undefined, name: string, - ): Promise; + ): Promise; }; // diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index a82385aae4..18481455ea 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-common'; import Knex from 'knex'; import path from 'path'; -import { DescriptorEnvelope } from '../ingestion'; +import { Entity } from '../ingestion'; import { Database } from './Database'; import { AddDatabaseLocation, @@ -247,8 +247,8 @@ describe('Database', () => { describe('entities', () => { it('can get all entities with empty filters list', async () => { const catalog = new Database(database, getVoidLogger()); - const e1: DescriptorEnvelope = { apiVersion: 'a', kind: 'b' }; - const e2: DescriptorEnvelope = { + const e1: Entity = { apiVersion: 'a', kind: 'b' }; + const e2: Entity = { apiVersion: 'a', kind: 'b', spec: { c: null }, @@ -271,7 +271,7 @@ describe('Database', () => { it('can get all specific entities for matching filters (naive case)', async () => { const catalog = new Database(database, getVoidLogger()); - const entities: DescriptorEnvelope[] = [ + const entities: Entity[] = [ { apiVersion: 'a', kind: 'b' }, { apiVersion: 'a', @@ -305,7 +305,7 @@ describe('Database', () => { it('can get all specific entities for matching filters with nulls (both missing and literal null value)', async () => { const catalog = new Database(database, getVoidLogger()); - const entities: DescriptorEnvelope[] = [ + const entities: Entity[] = [ { apiVersion: 'a', kind: 'b' }, { apiVersion: 'a', diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index 614f7d09bf..b7777d3ccb 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -24,7 +24,7 @@ import lodash from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from 'winston'; import { EntityFilters } from '../catalog'; -import { DescriptorEnvelope, EntityMeta } from '../ingestion'; +import { Entity, EntityMeta } from '../ingestion'; import { buildEntitySearch } from './search'; import { AddDatabaseLocation, @@ -54,9 +54,7 @@ function serializeMetadata(metadata: EntityMeta | undefined): string | null { return JSON.stringify(getStrippedMetadata(metadata)); } -function serializeSpec( - spec: DescriptorEnvelope['spec'], -): DbEntitiesRow['spec'] { +function serializeSpec(spec: Entity['spec']): DbEntitiesRow['spec'] { if (!spec) { return null; } @@ -66,7 +64,7 @@ function serializeSpec( function toEntityRow( locationId: string | undefined, - entity: DescriptorEnvelope, + entity: Entity, ): DbEntitiesRow { return { id: entity.metadata!.uid!, @@ -83,7 +81,7 @@ function toEntityRow( } function toEntityResponse(row: DbEntitiesRow): DbEntityResponse { - const entity: DescriptorEnvelope = { + const entity: Entity = { apiVersion: row.api_version, kind: row.kind, metadata: { @@ -94,7 +92,7 @@ function toEntityResponse(row: DbEntitiesRow): DbEntityResponse { }; if (row.metadata) { - const metadata = JSON.parse(row.metadata) as DescriptorEnvelope['metadata']; + const metadata = JSON.parse(row.metadata) as Entity['metadata']; entity.metadata = { ...entity.metadata, ...metadata }; } @@ -127,7 +125,9 @@ function generateUid(): string { } function generateEtag(): string { - return Buffer.from(uuidv4(), 'utf8').toString('base64').replace(/[^\w]/g, ''); + return Buffer.from(uuidv4(), 'utf8') + .toString('base64') + .replace(/[^\w]/g, ''); } /** @@ -374,7 +374,9 @@ export class Database { target, }); - return (await tx('locations').where({ id }).select())![0]; + return (await tx('locations') + .where({ id }) + .select())![0]; }); } @@ -422,7 +424,7 @@ export class Database { private async updateEntitiesSearch( tx: Knex.Transaction, entityId: string, - data: DescriptorEnvelope, + data: Entity, ): Promise { try { const entries = buildEntitySearch(entityId, data); diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index 7f646971db..4b7fa38326 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -19,8 +19,8 @@ import lodash from 'lodash'; import path from 'path'; import { Logger } from 'winston'; import { - DescriptorEnvelope, DescriptorParser, + Entity, LocationReader, ParserError, } from '../ingestion'; @@ -129,7 +129,7 @@ export class DatabaseManager { private static async refreshSingleEntity( database: Database, locationId: string, - entity: DescriptorEnvelope, + entity: Entity, logger: Logger, ): Promise { const { kind } = entity; @@ -163,10 +163,7 @@ export class DatabaseManager { }); } - private static entitiesAreEqual( - first: DescriptorEnvelope, - second: DescriptorEnvelope, - ) { + private static entitiesAreEqual(first: Entity, second: Entity) { const firstClone = lodash.cloneDeep(first); const secondClone = lodash.cloneDeep(second); diff --git a/plugins/catalog-backend/src/database/search.test.ts b/plugins/catalog-backend/src/database/search.test.ts index 26b20d1846..4c1df3429e 100644 --- a/plugins/catalog-backend/src/database/search.test.ts +++ b/plugins/catalog-backend/src/database/search.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DescriptorEnvelope } from '../ingestion'; +import { Entity } from '../ingestion'; import { buildEntitySearch, visitEntityPart } from './search'; import { DbEntitiesSearchRow } from './types'; @@ -99,7 +99,7 @@ describe('search', () => { describe('buildEntitySearch', () => { it('adds special keys even if missing', () => { - const input: DescriptorEnvelope = { + const input: Entity = { apiVersion: 'a', kind: 'b', }; @@ -116,7 +116,7 @@ describe('search', () => { }); it('adds prefix-stripped versions', () => { - const input: DescriptorEnvelope = { + const input: Entity = { apiVersion: 'a', kind: 'b', metadata: { diff --git a/plugins/catalog-backend/src/database/search.ts b/plugins/catalog-backend/src/database/search.ts index f54cfde7e6..e14ebd338f 100644 --- a/plugins/catalog-backend/src/database/search.ts +++ b/plugins/catalog-backend/src/database/search.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DescriptorEnvelope } from '../ingestion'; +import { Entity } from '../ingestion'; import { DbEntitiesSearchRow } from './types'; // Search entries that start with these prefixes, also get a shorthand without @@ -119,7 +119,7 @@ export function visitEntityPart( */ export function buildEntitySearch( entityId: string, - entity: DescriptorEnvelope, + entity: Entity, ): DbEntitiesSearchRow[] { // Start with some special keys that are always present because you want to // be able to easily search for null specifically diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 06e234545f..9fe4a523cc 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -15,7 +15,7 @@ */ import * as yup from 'yup'; -import { DescriptorEnvelope } from '../ingestion'; +import { Entity } from '../ingestion'; export type DbEntitiesRow = { id: string; @@ -32,12 +32,12 @@ export type DbEntitiesRow = { export type DbEntityRequest = { locationId?: string; - entity: DescriptorEnvelope; + entity: Entity; }; export type DbEntityResponse = { locationId?: string; - entity: DescriptorEnvelope; + entity: Entity; }; export type DbEntitiesSearchRow = { diff --git a/plugins/catalog-backend/src/ingestion/DescriptorParsers.ts b/plugins/catalog-backend/src/ingestion/DescriptorParsers.ts index 86283df666..6375351ebd 100644 --- a/plugins/catalog-backend/src/ingestion/DescriptorParsers.ts +++ b/plugins/catalog-backend/src/ingestion/DescriptorParsers.ts @@ -17,12 +17,7 @@ import { makeValidator } from '../validation'; import { ComponentDescriptorV1beta1Parser } from './descriptors/ComponentDescriptorV1beta1Parser'; import { DescriptorEnvelopeParser } from './descriptors/DescriptorEnvelopeParser'; -import { - DescriptorEnvelope, - DescriptorParser, - KindParser, - ParserError, -} from './types'; +import { DescriptorParser, Entity, KindParser, ParserError } from './types'; export class DescriptorParsers implements DescriptorParser { static create(): DescriptorParser { @@ -37,7 +32,7 @@ export class DescriptorParsers implements DescriptorParser { private readonly kindParsers: KindParser[], ) {} - async parse(descriptor: object): Promise { + async parse(descriptor: object): Promise { const envelope = await this.envelopeParser.parse(descriptor); for (const parser of this.kindParsers) { const parsed = await parser.tryParse(envelope); diff --git a/plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts b/plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts index 974b34aa8e..0934231a0c 100644 --- a/plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts +++ b/plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts @@ -15,9 +15,9 @@ */ import * as yup from 'yup'; -import { DescriptorEnvelope, KindParser, ParserError } from '../types'; +import { Entity, KindParser, ParserError } from '../types'; -export interface ComponentDescriptorV1beta1 extends DescriptorEnvelope { +export interface ComponentDescriptorV1beta1 extends Entity { spec: { type: string; }; @@ -41,9 +41,7 @@ export class ComponentDescriptorV1beta1Parser implements KindParser { }); } - async tryParse( - envelope: DescriptorEnvelope, - ): Promise { + async tryParse(envelope: Entity): Promise { if ( envelope.apiVersion !== 'backstage.io/v1beta1' || envelope.kind !== 'Component' diff --git a/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts b/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts index 1f21e2df9e..3833d22e77 100644 --- a/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts +++ b/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts @@ -16,13 +16,13 @@ import * as yup from 'yup'; import { Validators } from '../../validation'; -import { DescriptorEnvelope } from '../types'; +import { Entity } from '../types'; /** * Parses some raw structured data as a descriptor envelope */ export class DescriptorEnvelopeParser { - private schema: yup.Schema; + private schema: yup.Schema; constructor(validators: Validators) { const apiVersionSchema = yup @@ -160,8 +160,8 @@ export class DescriptorEnvelopeParser { .noUnknown(); } - async parse(data: any): Promise { - let result: DescriptorEnvelope; + async parse(data: any): Promise { + let result: Entity; try { result = await this.schema.validate(data, { strict: true }); } catch (e) { diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index e5083a65f1..45ee3bc638 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -87,7 +87,7 @@ export type EntityMeta = { * * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ */ -export type DescriptorEnvelope = { +export type Entity = { /** * The version of specification format for this particular entity that * this is written against. @@ -123,7 +123,7 @@ export type DescriptorParser = { * @returns A structure describing the parsed and validated descriptor * @throws An Error if the descriptor was malformed */ - parse(descriptor: object): Promise; + parse(descriptor: object): Promise; }; /** @@ -142,9 +142,7 @@ export type KindParser = { * @throws An Error if the type was handled and found to not be properly * formatted */ - tryParse( - envelope: DescriptorEnvelope, - ): Promise; + tryParse(envelope: Entity): Promise; }; export class ParserError extends Error { diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 8c29362015..210ad98577 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -18,7 +18,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; import { EntitiesCatalog, Location, LocationsCatalog } from '../catalog'; -import { DescriptorEnvelope } from '../ingestion'; +import { Entity } from '../ingestion'; import { createRouter } from './router'; class MockEntitiesCatalog implements EntitiesCatalog { @@ -37,7 +37,7 @@ class MockLocationsCatalog implements LocationsCatalog { describe('createRouter', () => { describe('entities', () => { it('happy path: lists entities', async () => { - const entities: DescriptorEnvelope[] = [{ apiVersion: 'a', kind: 'b' }]; + const entities: Entity[] = [{ apiVersion: 'a', kind: 'b' }]; const catalog = new MockEntitiesCatalog(); catalog.entities.mockResolvedValueOnce(entities); @@ -76,7 +76,7 @@ describe('createRouter', () => { describe('entityByUid', () => { it('can fetch entity by uid', async () => { - const entity: DescriptorEnvelope = { + const entity: Entity = { apiVersion: 'a', kind: 'b', metadata: { @@ -117,7 +117,7 @@ describe('createRouter', () => { describe('entityByName', () => { it('can fetch entity by name', async () => { - const entity: DescriptorEnvelope = { + const entity: Entity = { apiVersion: 'a', kind: 'b', metadata: { @@ -190,7 +190,9 @@ describe('createRouter', () => { }); const app = express().use(router); - const response = await request(app).post('/locations').send(location); + const response = await request(app) + .post('/locations') + .send(location); expect(response.status).toEqual(400); }); From 4e9d5a10c44c560b0dc4dc67ae8601b5ccba23c3 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 16:04:52 +0200 Subject: [PATCH 14/97] feature: add real get entity by name --- plugins/catalog/src/api/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/catalog/src/api/index.ts b/plugins/catalog/src/api/index.ts index 1bc739b56f..83c0eb1cfd 100644 --- a/plugins/catalog/src/api/index.ts +++ b/plugins/catalog/src/api/index.ts @@ -41,8 +41,10 @@ export class CatalogApi { return await response.json(); } async getEntityByName(name: string): Promise { - const entities = await this.getEntities(); - const entity = entities.find(e => e.metadata.name === name); + const response = await fetch( + `${this.apiOrigin}${this.basePath}/entities/by-name/Component/default/${name}`, + ); + const entity = await response.json(); if (entity) return entity; throw new Error(`'Entity not found: ${name}`); } From add55f64a86c41081901431765e7447eaa2af55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 11:52:50 +0200 Subject: [PATCH 15/97] Create the packages/catalog-model package --- packages/catalog-model/.eslintrc.js | 3 + packages/catalog-model/README.md | 12 + packages/catalog-model/package.json | 34 +++ packages/catalog-model/src/EntityPolicies.ts | 88 ++++++++ packages/catalog-model/src/entity/Entity.ts | 108 +++++++++ packages/catalog-model/src/entity/index.ts | 18 ++ .../policies/FieldFormatEntityPolicy.test.ts | 105 +++++++++ .../policies/FieldFormatEntityPolicy.ts | 91 ++++++++ .../ForeignRootFieldsEntityPolicy.test.ts | 52 +++++ .../policies/ForeignRootFieldsEntityPolicy.ts | 40 ++++ .../ReservedFieldsEntityPolicy.test.ts | 62 ++++++ .../policies/ReservedFieldsEntityPolicy.ts | 66 ++++++ .../policies/SchemaValidEntityPolicy.test.ts | 176 +++++++++++++++ .../policies/SchemaValidEntityPolicy.ts | 80 +++++++ .../src/entity/policies/index.ts | 20 ++ packages/catalog-model/src/index.ts | 21 ++ .../src/kinds/ComponentV1beta1.ts | 63 ++++++ packages/catalog-model/src/kinds/index.ts | 20 ++ packages/catalog-model/src/setupTests.ts | 15 ++ packages/catalog-model/src/types.ts | 32 +++ .../CommonValidatorFunctions.test.ts | 178 +++++++++++++++ .../validation/CommonValidatorFunctions.ts | 108 +++++++++ .../KubernetesValidatorFunctions.test.ts | 209 ++++++++++++++++++ .../KubernetesValidatorFunctions.ts | 86 +++++++ .../catalog-model/src/validation/index.ts | 20 ++ .../src/validation/makeValidator.ts | 38 ++++ .../catalog-model/src/validation/types.ts | 27 +++ ...0200520140700_location_update_log_table.ts | 5 +- .../src/ingestion/IngestionModels.ts | 73 ++++++ .../ingestion/descriptor/DescriptorParsers.ts | 45 ++++ .../src/ingestion/descriptor/index.ts | 18 ++ .../parsers/YamlDescriptorParser.ts | 64 ++++++ .../src/ingestion/descriptor/parsers/types.ts | 42 ++++ .../src/ingestion/source/LocationReaders.ts | 41 ++++ .../src/ingestion/source/index.ts | 20 ++ .../source/readers/FileLocationReader.ts | 35 +++ .../readers/GitHubLocationReader.test.ts | 92 ++++++++ .../source/readers/GitHubLocationReader.ts | 74 +++++++ .../src/ingestion/source/readers/types.ts | 29 +++ yarn.lock | 5 - 40 files changed, 2309 insertions(+), 6 deletions(-) create mode 100644 packages/catalog-model/.eslintrc.js create mode 100644 packages/catalog-model/README.md create mode 100644 packages/catalog-model/package.json create mode 100644 packages/catalog-model/src/EntityPolicies.ts create mode 100644 packages/catalog-model/src/entity/Entity.ts create mode 100644 packages/catalog-model/src/entity/index.ts create mode 100644 packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts create mode 100644 packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts create mode 100644 packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts create mode 100644 packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts create mode 100644 packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts create mode 100644 packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts create mode 100644 packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts create mode 100644 packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts create mode 100644 packages/catalog-model/src/entity/policies/index.ts create mode 100644 packages/catalog-model/src/index.ts create mode 100644 packages/catalog-model/src/kinds/ComponentV1beta1.ts create mode 100644 packages/catalog-model/src/kinds/index.ts create mode 100644 packages/catalog-model/src/setupTests.ts create mode 100644 packages/catalog-model/src/types.ts create mode 100644 packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts create mode 100644 packages/catalog-model/src/validation/CommonValidatorFunctions.ts create mode 100644 packages/catalog-model/src/validation/KubernetesValidatorFunctions.test.ts create mode 100644 packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts create mode 100644 packages/catalog-model/src/validation/index.ts create mode 100644 packages/catalog-model/src/validation/makeValidator.ts create mode 100644 packages/catalog-model/src/validation/types.ts create mode 100644 plugins/catalog-backend/src/ingestion/IngestionModels.ts create mode 100644 plugins/catalog-backend/src/ingestion/descriptor/DescriptorParsers.ts create mode 100644 plugins/catalog-backend/src/ingestion/descriptor/index.ts create mode 100644 plugins/catalog-backend/src/ingestion/descriptor/parsers/YamlDescriptorParser.ts create mode 100644 plugins/catalog-backend/src/ingestion/descriptor/parsers/types.ts create mode 100644 plugins/catalog-backend/src/ingestion/source/LocationReaders.ts create mode 100644 plugins/catalog-backend/src/ingestion/source/index.ts create mode 100644 plugins/catalog-backend/src/ingestion/source/readers/FileLocationReader.ts create mode 100644 plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.ts create mode 100644 plugins/catalog-backend/src/ingestion/source/readers/types.ts diff --git a/packages/catalog-model/.eslintrc.js b/packages/catalog-model/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/catalog-model/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/catalog-model/README.md b/packages/catalog-model/README.md new file mode 100644 index 0000000000..755b9ee63c --- /dev/null +++ b/packages/catalog-model/README.md @@ -0,0 +1,12 @@ +# Catalog Model + +Contains the core model types and validators/policies used by the Backstage catalog functionality. + +This package will be imported both by the frontend and backend parts of the catalog, +as well as by others that want to consume catalog data. + +## Links + +- (Default frontend part of the catalog)[https://github.com/spotify/backstage/tree/master/plugins/catalog] +- (Default backend part of the catalog)[https://github.com/spotify/backstage/tree/master/plugins/catalog-backend] +- (The Backstage homepage)[https://backstage.io] diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json new file mode 100644 index 0000000000..ffd8fdba22 --- /dev/null +++ b/packages/catalog-model/package.json @@ -0,0 +1,34 @@ +{ + "name": "@backstage/catalog-model", + "version": "0.1.1-alpha.6", + "main": "dist/index.esm.js", + "main:src": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "lodash": "^4.17.15", + "yup": "^0.28.5" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.6", + "@types/jest": "^25.2.2", + "@types/lodash": "^4.14.151", + "@types/yup": "^0.28.2", + "yaml": "^1.9.2" + }, + "files": [ + "dist/**/*.{js,d.ts}" + ] +} diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts new file mode 100644 index 0000000000..cedf0df13d --- /dev/null +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Entity, + FieldFormatEntityPolicy, + ForeignRootFieldsEntityPolicy, + ReservedFieldsEntityPolicy, + SchemaValidEntityPolicy, +} from './entity'; +import { ComponentV1beta1Policy } from './kinds'; +import { EntityPolicy } from './types'; + +// Helper that requires that all of a set of policies can be successfully +// applied +class AllEntityPolicies implements EntityPolicy { + constructor(private readonly policies: EntityPolicy[]) {} + + async apply(entity: Entity): Promise { + let result = entity; + for (const policy of this.policies) { + result = await policy.apply(entity); + } + return result; + } +} + +// Helper that requires that at least one of a set of policies can be +// successfully applied +class AnyEntityPolicy implements EntityPolicy { + constructor(private readonly policies: EntityPolicy[]) {} + + async apply(entity: Entity): Promise { + for (const policy of this.policies) { + try { + return await policy.apply(entity); + } catch { + continue; + } + } + throw new Error(`The entity did not match any known policy`); + } +} + +export class EntityPolicies implements EntityPolicy { + private readonly policy: EntityPolicy; + + static defaultPolicies(): EntityPolicy { + return EntityPolicies.allOf([ + EntityPolicies.allOf([ + new SchemaValidEntityPolicy(), + new ForeignRootFieldsEntityPolicy(), + new FieldFormatEntityPolicy(), + new ReservedFieldsEntityPolicy(), + ]), + EntityPolicies.anyOf([new ComponentV1beta1Policy()]), + ]); + } + + static allOf(policies: EntityPolicy[]): EntityPolicy { + return new AllEntityPolicies(policies); + } + + static anyOf(policies: EntityPolicy[]): EntityPolicy { + return new AnyEntityPolicy(policies); + } + + constructor(policy: EntityPolicy = EntityPolicies.defaultPolicies()) { + this.policy = policy; + } + + apply(entity: Entity): Promise { + return this.policy.apply(entity); + } +} diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts new file mode 100644 index 0000000000..6f57626749 --- /dev/null +++ b/packages/catalog-model/src/entity/Entity.ts @@ -0,0 +1,108 @@ +/* + * 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. + */ + +/** + * The format envelope that's common to all versions/kinds of entity. + * + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ + */ +export type Entity = { + /** + * The version of specification format for this particular entity that + * this is written against. + */ + apiVersion: string; + + /** + * The high level entity type being described. + */ + kind: string; + + /** + * Optional metadata related to the entity. + */ + metadata?: EntityMeta; + + /** + * The specification data describing the entity itself. + */ + spec?: object; +}; + +/** + * Metadata fields common to all versions/kinds of entity. + * + * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ + */ +export type EntityMeta = { + /** + * A globally unique ID for the entity. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. The field can (optionally) be specified when performing + * update or delete operations, but the server is free to reject requests + * that do so in such a way that it breaks semantics. + */ + uid?: string; + + /** + * An opaque string that changes for each update operation to any part of + * the entity, including metadata. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. The field can (optionally) be specified when performing + * update or delete operations, and the server will then reject the + * operation if it does not match the current stored value. + */ + etag?: string; + + /** + * A positive nonzero number that indicates the current generation of data + * for this entity; the value is incremented each time the spec changes. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. + */ + generation?: number; + + /** + * The name of the entity. + * + * Must be uniqe within the catalog at any given point in time, for any + * given namespace, for any given kind. + */ + name?: string; + + /** + * The namespace that the entity belongs to. + */ + namespace?: string; + + /** + * Key/value pairs of identifying information attached to the entity. + */ + labels?: Record; + + /** + * Key/value pairs of non-identifying auxiliary information attached to the + * entity. + */ + annotations?: Record; +}; diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts new file mode 100644 index 0000000000..9e96021336 --- /dev/null +++ b/packages/catalog-model/src/entity/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export type { Entity, EntityMeta } from './Entity'; +export * from './policies'; diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts new file mode 100644 index 0000000000..d81b1155be --- /dev/null +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; +import { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy'; + +describe('FieldFormatEntityPolicy', () => { + let data: any; + let policy: FieldFormatEntityPolicy; + + beforeEach(() => { + data = yaml.parse(` + apiVersion: backstage.io/v1beta1 + kind: Component + metadata: + uid: e01199ab-08cc-44c2-8e19-5c29ded82521 + etag: lsndfkjsndfkjnsdfkjnsd== + generation: 13 + name: my-component-yay + namespace: the-namespace + labels: + backstage.io/custom: ValueStuff + annotations: + example.com/bindings: are-secret + spec: + custom: stuff + `); + policy = new FieldFormatEntityPolicy(); + }); + + it('works for the happy path', async () => { + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad apiVersion', async () => { + data.apiVersion = 7; + await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + data.apiVersion = 'a#b'; + await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + }); + + it('rejects bad kind', async () => { + data.kind = 7; + await expect(policy.apply(data)).rejects.toThrow(/kind/); + data.kind = 'a#b'; + await expect(policy.apply(data)).rejects.toThrow(/kind/); + }); + + it('handles missing metadata gracefully', async () => { + delete data.medatata; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('handles missing spec gracefully', async () => { + delete data.spec; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad name', async () => { + data.metadata.name = 7; + await expect(policy.apply(data)).rejects.toThrow(/name.*7/); + data.metadata.name = 'a'.repeat(1000); + await expect(policy.apply(data)).rejects.toThrow(/name.*aaaa/); + }); + + it('rejects bad namespace', async () => { + data.metadata.namespace = 7; + await expect(policy.apply(data)).rejects.toThrow(/namespace.*7/); + data.metadata.namespace = 'a'.repeat(1000); + await expect(policy.apply(data)).rejects.toThrow(/namespace.*aaaa/); + }); + + it('rejects bad label key', async () => { + data.metadata.labels['a#b'] = 'value'; + await expect(policy.apply(data)).rejects.toThrow(/label.*a#b/i); + }); + + it('rejects bad label value', async () => { + data.metadata.labels.a = 'a#b'; + await expect(policy.apply(data)).rejects.toThrow(/label.*a#b/i); + }); + + it('rejects bad annotation key', async () => { + data.metadata.annotations['a#b'] = 'value'; + await expect(policy.apply(data)).rejects.toThrow(/annotation.*a#b/i); + }); + + it('rejects bad annotation value', async () => { + data.metadata.annotations.a = 7; + await expect(policy.apply(data)).rejects.toThrow(/annotation.*7/i); + }); +}); diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts new file mode 100644 index 0000000000..1f94354f3f --- /dev/null +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityPolicy } from '../../types'; +import { makeValidator, Validators } from '../../validation'; +import { Entity } from '../Entity'; + +/** + * Ensures that the format of individual fields of the entity envelope + * is valid. + * + * This does not take into account machine generated fields such as uid, etag + * and generation. + */ +export class FieldFormatEntityPolicy implements EntityPolicy { + private readonly validators: Validators; + + constructor(validators: Validators = makeValidator()) { + this.validators = validators; + } + + async apply(entity: Entity): Promise { + function require( + field: string, + value: any, + validator: (value: any) => boolean, + ) { + if (value === undefined || value === null) { + throw new Error(`${field} must have a value`); + } + + let isValid: boolean; + try { + isValid = validator(value); + } catch (e) { + throw new Error(`${field} could not be validated, ${e}`); + } + + if (!isValid) { + throw new Error(`${field} "${value}" is not valid`); + } + } + + function optional( + field: string, + value: any, + validator: (value: any) => boolean, + ) { + return value === undefined || require(field, value, validator); + } + + require('apiVersion', entity.apiVersion, this.validators.isValidApiVersion); + require('kind', entity.kind, this.validators.isValidKind); + + optional( + 'metadata.name', + entity.metadata?.name, + this.validators.isValidEntityName, + ); + optional( + 'metadata.namespace', + entity.metadata?.namespace, + this.validators.isValidNamespace, + ); + + for (const [k, v] of Object.entries(entity.metadata?.labels ?? [])) { + require(`labels.${k}`, k, this.validators.isValidLabelKey); + require(`labels.${k}`, v, this.validators.isValidLabelValue); + } + + for (const [k, v] of Object.entries(entity.metadata?.annotations ?? [])) { + require(`annotations.${k}`, k, this.validators.isValidAnnotationKey); + require(`annotations.${k}`, v, this.validators.isValidAnnotationValue); + } + + return entity; + } +} diff --git a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts new file mode 100644 index 0000000000..98299259f8 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; +import { ForeignRootFieldsEntityPolicy } from './ForeignRootFieldsEntityPolicy'; + +describe('ForeignRootFieldsEntityPolicy', () => { + let data: any; + let policy: ForeignRootFieldsEntityPolicy; + + beforeEach(() => { + data = yaml.parse(` + apiVersion: backstage.io/v1beta1 + kind: Component + metadata: + uid: e01199ab-08cc-44c2-8e19-5c29ded82521 + etag: lsndfkjsndfkjnsdfkjnsd== + generation: 13 + name: my-component-yay + namespace: the-namespace + labels: + backstage.io/custom: ValueStuff + annotations: + example.com/bindings: are-secret + spec: + custom: stuff + `); + policy = new ForeignRootFieldsEntityPolicy(); + }); + + it('works for the happy path', async () => { + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects unknown root fields', async () => { + data.spec2 = {}; + await expect(policy.apply(data)).rejects.toThrow(/spec2/i); + }); +}); diff --git a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts new file mode 100644 index 0000000000..a4733e9a42 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityPolicy } from '../../types'; +import { Entity } from '../Entity'; + +const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec']; + +/** + * Ensures that there are no foreign root fields in the entity. + */ +export class ForeignRootFieldsEntityPolicy implements EntityPolicy { + private readonly knownFields: string[]; + + constructor(knownFields: string[] = defaultKnownFields) { + this.knownFields = knownFields; + } + + async apply(entity: Entity): Promise { + for (const field of Object.keys(entity)) { + if (!this.knownFields.includes(field)) { + throw new Error(`Unknown field ${field}`); + } + } + return entity; + } +} diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts new file mode 100644 index 0000000000..348eabbdac --- /dev/null +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; +import { ReservedFieldsEntityPolicy } from './ReservedFieldsEntityPolicy'; + +describe('ReservedFieldsEntityPolicy', () => { + let data: any; + let policy: ReservedFieldsEntityPolicy; + + beforeEach(() => { + data = yaml.parse(` + apiVersion: backstage.io/v1beta1 + kind: Component + metadata: + uid: e01199ab-08cc-44c2-8e19-5c29ded82521 + etag: lsndfkjsndfkjnsdfkjnsd== + generation: 13 + name: my-component-yay + namespace: the-namespace + labels: + backstage.io/custom: ValueStuff + annotations: + example.com/bindings: are-secret + spec: + custom: stuff + `); + policy = new ReservedFieldsEntityPolicy(); + }); + + it('works for the happy path', async () => { + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects reserved keys in the spec root', async () => { + data.spec.apiVersion = 'a/b'; + await expect(policy.apply(data)).rejects.toThrow(/spec.*apiVersion/i); + }); + + it('rejects reserved keys in labels', async () => { + data.metadata.labels.apiVersion = 'a'; + await expect(policy.apply(data)).rejects.toThrow(/label.*apiVersion/i); + }); + + it('rejects reserved keys in annotations', async () => { + data.metadata.annotations.apiVersion = 'a'; + await expect(policy.apply(data)).rejects.toThrow(/annotation.*apiVersion/i); + }); +}); diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts new file mode 100644 index 0000000000..be2f732ca4 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts @@ -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. + */ + +import { EntityPolicy } from '../../types'; +import { Entity } from '../Entity'; + +const DEFAULT_RESERVED_ENTITY_FIELDS = [ + 'apiVersion', + 'kind', + 'uid', + 'etag', + 'generation', + 'name', + 'namespace', + 'labels', + 'annotations', + 'spec', +]; + +/** + * Ensures that fields are not given certain reserved names. + */ +export class ReservedFieldsEntityPolicy implements EntityPolicy { + private readonly reservedFields: string[]; + + constructor(fields?: string[]) { + this.reservedFields = [ + ...(fields ?? []), + ...DEFAULT_RESERVED_ENTITY_FIELDS, + ]; + } + + async apply(entity: Entity): Promise { + for (const field of this.reservedFields) { + if (entity.spec?.hasOwnProperty(field)) { + throw new Error( + `The spec may not contain the field ${field}, because it has reserved meaning`, + ); + } + if (entity.metadata?.labels?.hasOwnProperty(field)) { + throw new Error( + `A label may not have the field ${field}, because it has reserved meaning`, + ); + } + if (entity.metadata?.annotations?.hasOwnProperty(field)) { + throw new Error( + `An annotation may not have the field ${field}, because it has reserved meaning`, + ); + } + } + return entity; + } +} diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts new file mode 100644 index 0000000000..b9d1165a60 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -0,0 +1,176 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; +import { Entity } from '../Entity'; +import { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy'; + +describe('SchemaValidEntityPolicy', () => { + let data: any; + let policy: SchemaValidEntityPolicy; + + beforeEach(() => { + data = yaml.parse(` + apiVersion: backstage.io/v1beta1 + kind: Component + metadata: + uid: e01199ab-08cc-44c2-8e19-5c29ded82521 + etag: lsndfkjsndfkjnsdfkjnsd== + generation: 13 + name: my-component-yay + namespace: the-namespace + labels: + backstage.io/custom: ValueStuff + annotations: + example.com/bindings: are-secret + spec: + custom: stuff + `); + policy = new SchemaValidEntityPolicy(); + }); + + it('works for the happy path', async () => { + await expect(policy.apply(data)).resolves.toBe(data); + }); + + // + // apiVersion and kind + // + + it('rejects wrong root type', async () => { + await expect(policy.apply((7 as unknown) as Entity)).rejects.toThrow( + /object/, + ); + }); + + it('rejects missing apiVersion', async () => { + delete data.apiVersion; + await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + }); + + it('rejects bad apiVersion type', async () => { + data.apiVersion = 7; + await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + }); + + it('rejects missing kind', async () => { + delete data.kind; + await expect(policy.apply(data)).rejects.toThrow(/kind/); + }); + + it('rejects bad kind type', async () => { + data.kind = 7; + await expect(policy.apply(data)).rejects.toThrow(/kind/); + }); + + // + // metadata + // + + it('accepts missing metadata', async () => { + delete data.medatata; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad metadata type', async () => { + data.metadata = 7; + await expect(policy.apply(data)).rejects.toThrow(/metadata/); + }); + + it('accepts missing uid', async () => { + delete data.metadata.uid; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad uid type', async () => { + data.metadata.uid = 7; + await expect(policy.apply(data)).rejects.toThrow(/uid/); + }); + + it('accepts missing etag', async () => { + delete data.metadata.etag; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad etag type', async () => { + data.metadata.etag = 7; + await expect(policy.apply(data)).rejects.toThrow(/etag/); + }); + + it('accepts missing generation', async () => { + delete data.metadata.generation; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad generation type', async () => { + data.metadata.generation = 'a'; + await expect(policy.apply(data)).rejects.toThrow(/generation/); + }); + + it('accepts missing name', async () => { + delete data.metadata.name; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad name type', async () => { + data.metadata.name = 7; + await expect(policy.apply(data)).rejects.toThrow(/name/); + }); + + it('accepts missing namespace', async () => { + delete data.metadata.namespace; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad namespace type', async () => { + data.metadata.namespace = 7; + await expect(policy.apply(data)).rejects.toThrow(/namespace/); + }); + + it('accepts missing labels', async () => { + delete data.metadata.labels; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad labels type', async () => { + data.metadata.labels = 7; + await expect(policy.apply(data)).rejects.toThrow(/labels/); + }); + + it('accepts missing annotations', async () => { + delete data.metadata.annotations; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects bad annotations type', async () => { + data.metadata.annotations = 7; + await expect(policy.apply(data)).rejects.toThrow(/annotations/); + }); + + // + // spec + // + + it('accepts missing spec', async () => { + delete data.spec; + await expect(policy.apply(data)).resolves.toBe(data); + }); + + it('rejects non-object spec', async () => { + data.spec = 7; + await expect(policy.apply(data)).rejects.toThrow(/spec/); + }); +}); diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts new file mode 100644 index 0000000000..7c0f5c20b6 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as yup from 'yup'; +import { EntityPolicy } from '../../types'; +import { Entity } from '../Entity'; + +const DEFAULT_ENTITY_SCHEMA = yup.object({ + apiVersion: yup.string().required(), + kind: yup.string().required(), + metadata: yup + .object({ + uid: yup + .string() + .notRequired() + .test( + 'metadata.uid', + 'The uid must not be empty', + value => value === undefined || value.length > 0, + ), + etag: yup + .string() + .notRequired() + .test( + 'metadata.etag', + 'The etag must not be empty', + value => value === undefined || value.length > 0, + ), + generation: yup + .number() + .notRequired() + .test( + 'metadata.generation', + 'The generation must be an integer greater than zero', + value => value === undefined || (value === (value | 0) && value > 0), + ), + name: yup.string().notRequired(), + namespace: yup.string().notRequired(), + labels: yup.object>().notRequired(), + annotations: yup.object>().notRequired(), + }) + .notRequired(), + spec: yup.object({}).notRequired(), +}); + +/** + * Ensures that the entity spec is valid according to a schema. + * + * This should be the first policy in the list, to ensure that other downstream + * policies can work with a structure that is at least valid in therms of the + * typescript type. + */ +export class SchemaValidEntityPolicy implements EntityPolicy { + private readonly schema: yup.Schema; + + constructor(schema: yup.Schema = DEFAULT_ENTITY_SCHEMA) { + this.schema = schema; + } + + async apply(entity: Entity): Promise { + try { + return await this.schema.validate(entity, { strict: true }); + } catch (e) { + throw new Error(`Malformed envelope, ${e}`); + } + } +} diff --git a/packages/catalog-model/src/entity/policies/index.ts b/packages/catalog-model/src/entity/policies/index.ts new file mode 100644 index 0000000000..f43aa68049 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy'; +export { ForeignRootFieldsEntityPolicy } from './ForeignRootFieldsEntityPolicy'; +export { ReservedFieldsEntityPolicy } from './ReservedFieldsEntityPolicy'; +export { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy'; diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts new file mode 100644 index 0000000000..fb51461053 --- /dev/null +++ b/packages/catalog-model/src/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export * from './entity'; +export { EntityPolicies } from './EntityPolicies'; +export * from './kinds'; +export type { EntityPolicy } from './types'; +export * from './validation'; diff --git a/packages/catalog-model/src/kinds/ComponentV1beta1.ts b/packages/catalog-model/src/kinds/ComponentV1beta1.ts new file mode 100644 index 0000000000..b0a3f627a5 --- /dev/null +++ b/packages/catalog-model/src/kinds/ComponentV1beta1.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as yup from 'yup'; +import type { Entity, EntityMeta } from '../entity/Entity'; +import type { EntityPolicy } from '../types'; + +const API_VERSION = 'backstage.io/v1beta1'; +const KIND = 'Component'; + +export interface ComponentV1beta1 extends Entity { + apiVersion: typeof API_VERSION; + kind: typeof KIND; + metadata: EntityMeta & { + name: string; + }; + spec: { + type: string; + }; +} + +export class ComponentV1beta1Policy implements EntityPolicy { + private schema: yup.Schema; + + constructor() { + this.schema = yup.object>({ + metadata: yup + .object({ + name: yup.string().required(), + }) + .required(), + spec: yup + .object({ + type: yup.string().required(), + }) + .required(), + }); + } + + async apply(envelope: Entity): Promise { + if ( + envelope.apiVersion !== 'backstage.io/v1beta1' || + envelope.kind !== 'Component' + ) { + throw new Error('Unsupported apiVersion / kind'); + } + + return await this.schema.validate(envelope, { strict: true }); + } +} diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts new file mode 100644 index 0000000000..97d22c14a5 --- /dev/null +++ b/packages/catalog-model/src/kinds/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ComponentV1beta1 } from './ComponentV1beta1'; +export { ComponentV1beta1Policy } from './ComponentV1beta1'; +export { ComponentV1beta1 as Component }; +export { ComponentV1beta1 }; diff --git a/packages/catalog-model/src/setupTests.ts b/packages/catalog-model/src/setupTests.ts new file mode 100644 index 0000000000..f3b69cc361 --- /dev/null +++ b/packages/catalog-model/src/setupTests.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts new file mode 100644 index 0000000000..1d581cf23f --- /dev/null +++ b/packages/catalog-model/src/types.ts @@ -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. + */ + +import type { Entity } from './entity/Entity'; + +/** + * A policy for validation or mutation to be applied to entities as they are + * entering the system. + */ +export type EntityPolicy = { + /** + * Applies validation or mutation on an entity. + * + * @param entity The entity, as validated/mutated so far in the policy tree + * @returns The incoming entity, or a mutated version of the same + * @throws An error if the entity should be rejected + */ + apply(entity: Entity): Promise; +}; diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts new file mode 100644 index 0000000000..200e90b406 --- /dev/null +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -0,0 +1,178 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonValidatorFunctions } from './CommonValidatorFunctions'; + +describe('CommonValidatorFunctions', () => { + describe('isValidPrefixAndOrSuffix', () => { + it('only accepts strings', () => { + expect( + CommonValidatorFunctions.isValidPrefixAndOrSuffix( + null, + '/', + () => true, + () => true, + ), + ).toBe(false); + expect( + CommonValidatorFunctions.isValidPrefixAndOrSuffix( + 7, + '/', + () => true, + () => true, + ), + ).toBe(false); + expect( + CommonValidatorFunctions.isValidPrefixAndOrSuffix( + () => 'hello', + '/', + () => true, + () => true, + ), + ).toBe(false); + }); + + it('only accepts one or two parts', () => { + expect( + CommonValidatorFunctions.isValidPrefixAndOrSuffix( + 'a', + '/', + () => true, + () => true, + ), + ).toBe(true); + expect( + CommonValidatorFunctions.isValidPrefixAndOrSuffix( + 'a/b', + '/', + () => true, + () => true, + ), + ).toBe(true); + expect( + CommonValidatorFunctions.isValidPrefixAndOrSuffix( + 'a/b/c', + '/', + () => true, + () => true, + ), + ).toBe(false); + }); + + it('checks the prefix and suffix', () => { + expect( + CommonValidatorFunctions.isValidPrefixAndOrSuffix( + 'a/b', + '/', + () => true, + () => true, + ), + ).toBe(true); + expect( + CommonValidatorFunctions.isValidPrefixAndOrSuffix( + 'a/b', + '/', + () => false, + () => true, + ), + ).toBe(false); + expect( + CommonValidatorFunctions.isValidPrefixAndOrSuffix( + 'a/b', + '/', + () => true, + () => false, + ), + ).toBe(false); + }); + }); + + it.each([ + [null, true], + [undefined, false], + [1, true], + ['a', true], + [() => 'a', false], + [Symbol('a'), false], + [[], true], + [[1], true], + [[undefined], false], + [{}, true], + [{ a: 1 }, true], + [{ a: undefined }, false], + ] as [any, boolean][])(`isJsonSafe %p ? %p`, (value, result) => { + expect(CommonValidatorFunctions.isJsonSafe(value)).toBe(result); + }); + + it.each([ + [null, false], + [7, false], + ['', false], + ['a', true], + ['a-b', true], + ['-a-b', false], + ['a-b-', false], + ['a--b', false], + ['a_b', false], + ['adam.bertil.caesar', true], + ['adam.ber-til.caesar', true], + ['adam.-bertil.caesar', false], + ['adam.bertil-.caesar', false], + ['adam/bertil.caesar', false], + [`a.${'b'.repeat(63)}.c`, true], + [`a.${'b'.repeat(64)}.c`, false], + [ + `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(61)}`, + true, + ], + [ + `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(62)}`, + false, + ], + ])(`isValidDnsSubdomain %p ? %p`, (value, result) => { + expect(CommonValidatorFunctions.isValidDnsSubdomain(value)).toBe(result); + }); + + it.each([ + [null, false], + [7, false], + ['', false], + ['a', true], + ['a-b', true], + ['-a-b', false], + ['a-b-', false], + ['a--b', false], + ['a_b', false], + [`${'a'.repeat(63)}`, true], + [`${'a'.repeat(64)}`, false], + ])(`isValidDnsLabel %p ? %p`, (value, result) => { + expect(CommonValidatorFunctions.isValidDnsLabel(value)).toBe(result); + }); + + it.each([ + ['', ''], + ['a', 'a'], + ['a-b', 'ab'], + ['-a-b', 'ab'], + ['a_b', 'ab'], + [`${'a'.repeat(6000)}`, `${'a'.repeat(6000)}`], + ['_:;>!"#€', ''], + ])(`normalizeToLowercaseAlphanum %p ? %p`, (value, result) => { + expect(CommonValidatorFunctions.normalizeToLowercaseAlphanum(value)).toBe( + result, + ); + }); +}); diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts new file mode 100644 index 0000000000..96a91aca06 --- /dev/null +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import lodash from 'lodash'; + +/** + * Contains various helper validation and normalization functions that can be + * composed to form a Validator. + */ +export class CommonValidatorFunctions { + /** + * Checks that the value is on the form or , and validates + * those parts separately. + * + * @param value The value to check + * @param separator The separator between parts + * @param isValidPrefix Checks that the part before the separator is valid, if present + * @param isValidSuffix Checks that the part after the separator (or the entire value if there is no separator) is valid + */ + static isValidPrefixAndOrSuffix( + value: any, + separator: string, + isValidPrefix: (value: string) => boolean, + isValidSuffix: (value: string) => boolean, + ): boolean { + if (typeof value !== 'string') { + return false; + } + + const parts = value.split(separator); + if (parts.length === 1) { + return isValidSuffix(parts[0]); + } else if (parts.length === 2) { + return isValidPrefix(parts[0]) && isValidSuffix(parts[1]); + } + + return false; + } + + /** + * Checks that the value can be safely transferred as JSON. + * + * @param value The value to check + */ + static isJsonSafe(value: any): boolean { + try { + return lodash.isEqual(value, JSON.parse(JSON.stringify(value))); + } catch { + return false; + } + } + + /** + * Checks that the value is a valid DNS subdomain name. + * + * @param value The value to check + * @see https://tools.ietf.org/html/rfc1123 + */ + static isValidDnsSubdomain(value: any): boolean { + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 253 && + value.split('.').every(CommonValidatorFunctions.isValidDnsLabel) + ); + } + + /** + * Checks that the value is a valid DNS label. + * + * @param value The value to check + * @see https://tools.ietf.org/html/rfc1123 + */ + static isValidDnsLabel(value: any): boolean { + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 63 && + /^[a-z0-9]+(\-[a-z0-9]+)*$/.test(value) + ); + } + + /** + * Normalizes by keeping only a-z, A-Z, and 0-9; and converts to lowercase. + * + * @param value The value to normalize + */ + static normalizeToLowercaseAlphanum(value: string): string { + return value + .split('') + .filter(x => /[a-zA-Z0-9]/.test(x)) + .join('') + .toLowerCase(); + } +} diff --git a/packages/catalog-model/src/validation/KubernetesValidatorFunctions.test.ts b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.test.ts new file mode 100644 index 0000000000..d0673085b4 --- /dev/null +++ b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.test.ts @@ -0,0 +1,209 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions'; + +describe('KubernetesValidatorFunctions', () => { + it.each([ + [7, false], + [null, false], + ['', false], + ['a', true], + ['AZ09', true], + ['a'.repeat(63), true], + ['a'.repeat(64), false], + ['a-b', false], + ['a_b', false], + ['a.b', false], + ['a/a', true], + ['a/aAb5C', true], + ['a-b.c/v1', true], + ['a--b.c/v1', false], + [ + `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( + 61, + )}/v1`, + true, + ], + [ + `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( + 62, + )}/v1`, + false, + ], + [`a/${'a'.repeat(63)}`, true], + [`a/${'a'.repeat(64)}`, false], + ])(`isValidApiVersion %p ? %p`, (value, matches) => { + expect(KubernetesValidatorFunctions.isValidApiVersion(value)).toBe(matches); + }); + + it.each([ + [7, false], + [null, false], + ['', false], + ['a', true], + ['AZ09', true], + ['9AZ', false], + ['a'.repeat(63), true], + ['a'.repeat(64), false], + ['a-b', false], + ])(`isValidKind %p ? %p`, (value, matches) => { + expect(KubernetesValidatorFunctions.isValidKind(value)).toBe(matches); + }); + + it.each([ + [7, false], + [null, false], + ['', false], + ['a', true], + ['AZ09', true], + ['a'.repeat(63), true], + ['a'.repeat(64), false], + ['a/b', false], + ['a-b', true], + ['-a-b', false], + ['a-b-', false], + ['a--b', false], + ['a_b', true], + ['a.b', true], + ])(`isValidObjectName %p ? %p`, (value, matches) => { + expect(KubernetesValidatorFunctions.isValidObjectName(value)).toBe(matches); + }); + + it.each([ + [7, false], + [null, false], + ['', false], + ['a', true], + ['AZ09', false], + ['a'.repeat(63), true], + ['a'.repeat(64), false], + ['a/b', false], + ['a-b', true], + ['-a-b', false], + ['a-b-', false], + ['a--b', false], + ['a_b', false], + ['a.b', false], + ])(`isValidNamespace %p ? %p`, (value, matches) => { + expect(KubernetesValidatorFunctions.isValidNamespace(value)).toBe(matches); + }); + + it.each([ + [7, false], + [null, false], + ['', false], + ['a', true], + ['AZ09', true], + ['a'.repeat(63), true], + ['a'.repeat(64), false], + ['a/b', true], + ['a-b', true], + ['-a-b', false], + ['a-b-', false], + ['a--b', false], + ['a_b', true], + ['a.b', true], + ['a/a', true], + ['a-b.c/a', true], + ['a--b.c/a', false], + [ + `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( + 61, + )}/a`, + true, + ], + [ + `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( + 62, + )}/a`, + false, + ], + [`a/${'a'.repeat(63)}`, true], + [`a/${'a'.repeat(64)}`, false], + ])(`isValidLabelKey %p ? %p`, (value, matches) => { + expect(KubernetesValidatorFunctions.isValidLabelKey(value)).toBe(matches); + }); + + it.each([ + [7, false], + [null, false], + ['', true], + ['a', true], + ['AZ09', true], + ['a'.repeat(63), true], + ['a'.repeat(64), false], + ['a/b', false], + ['a-b', true], + ['-a-b', false], + ['a-b-', false], + ['a--b', false], + ['a_b', true], + ['a.b', true], + ])(`isValidLabelValue %p ? %p`, (value, matches) => { + expect(KubernetesValidatorFunctions.isValidLabelValue(value)).toBe(matches); + }); + + it.each([ + [7, false], + [null, false], + ['', false], + ['a', true], + ['AZ09', true], + ['a'.repeat(63), true], + ['a'.repeat(64), false], + ['a/b', true], + ['a-b', true], + ['-a-b', false], + ['a-b-', false], + ['a--b', false], + ['a_b', true], + ['a.b', true], + ['a/a', true], + ['a-b.c/a', true], + ['a--b.c/a', false], + [ + `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( + 61, + )}/a`, + true, + ], + [ + `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( + 62, + )}/a`, + false, + ], + [`a/${'a'.repeat(63)}`, true], + [`a/${'a'.repeat(64)}`, false], + ])(`isValidAnnotationKey %p ? %p`, (value, matches) => { + expect(KubernetesValidatorFunctions.isValidAnnotationKey(value)).toBe( + matches, + ); + }); + + it.each([ + [7, false], + [null, false], + ['', true], + ['a', true], + ['/'.repeat(6000), true], + ])(`isValidAnnotationValue %p ? %p`, (value, matches) => { + expect(KubernetesValidatorFunctions.isValidAnnotationValue(value)).toBe( + matches, + ); + }); +}); diff --git a/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts new file mode 100644 index 0000000000..fa938f5fcb --- /dev/null +++ b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonValidatorFunctions } from './CommonValidatorFunctions'; + +/** + * Contains validation functions that match the Kubernetes spec, usable to + * build a catalog that is compatible with those rule sets. + * + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set + */ +export class KubernetesValidatorFunctions { + static isValidApiVersion(value: any): boolean { + return CommonValidatorFunctions.isValidPrefixAndOrSuffix( + value, + '/', + CommonValidatorFunctions.isValidDnsSubdomain, + n => n.length >= 1 && n.length <= 63 && /^[a-z0-9A-Z]+$/.test(n), + ); + } + + static isValidKind(value: any): boolean { + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 63 && + /^[a-zA-Z][a-z0-9A-Z]*$/.test(value) + ); + } + + static isValidObjectName(value: any): boolean { + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 63 && + /^[a-z0-9A-Z]+([-_.][a-z0-9A-Z]+)*$/.test(value) + ); + } + + static isValidNamespace(value: any): boolean { + return CommonValidatorFunctions.isValidDnsLabel(value); + } + + static isValidLabelKey(value: any): boolean { + return CommonValidatorFunctions.isValidPrefixAndOrSuffix( + value, + '/', + CommonValidatorFunctions.isValidDnsSubdomain, + KubernetesValidatorFunctions.isValidObjectName, + ); + } + + static isValidLabelValue(value: any): boolean { + return ( + value === '' || KubernetesValidatorFunctions.isValidObjectName(value) + ); + } + + static isValidAnnotationKey(value: any): boolean { + return CommonValidatorFunctions.isValidPrefixAndOrSuffix( + value, + '/', + CommonValidatorFunctions.isValidDnsSubdomain, + KubernetesValidatorFunctions.isValidObjectName, + ); + } + + static isValidAnnotationValue(value: any): boolean { + return typeof value === 'string'; + } +} diff --git a/packages/catalog-model/src/validation/index.ts b/packages/catalog-model/src/validation/index.ts new file mode 100644 index 0000000000..d679a5323c --- /dev/null +++ b/packages/catalog-model/src/validation/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { CommonValidatorFunctions } from './CommonValidatorFunctions'; +export { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions'; +export { makeValidator } from './makeValidator'; +export type { Validators } from './types'; diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts new file mode 100644 index 0000000000..7ca01365e0 --- /dev/null +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonValidatorFunctions } from './CommonValidatorFunctions'; +import { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions'; +import { Validators } from './types'; + +const defaultValidators: Validators = { + isValidApiVersion: KubernetesValidatorFunctions.isValidApiVersion, + isValidKind: KubernetesValidatorFunctions.isValidKind, + isValidEntityName: KubernetesValidatorFunctions.isValidObjectName, + isValidNamespace: KubernetesValidatorFunctions.isValidNamespace, + normalizeEntityName: CommonValidatorFunctions.normalizeToLowercaseAlphanum, + isValidLabelKey: KubernetesValidatorFunctions.isValidLabelKey, + isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue, + isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey, + isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue, +}; + +export function makeValidator(overrides: Partial = {}): Validators { + return { + ...defaultValidators, + ...overrides, + }; +} diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts new file mode 100644 index 0000000000..81209bfb75 --- /dev/null +++ b/packages/catalog-model/src/validation/types.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export type Validators = { + isValidApiVersion(value: any): boolean; + isValidKind(value: any): boolean; + isValidEntityName(value: any): boolean; + isValidNamespace(value: any): boolean; + normalizeEntityName(value: string): string; + isValidLabelKey(value: any): boolean; + isValidLabelValue(value: any): boolean; + isValidAnnotationKey(value: any): boolean; + isValidAnnotationValue(value: any): boolean; +}; diff --git a/plugins/catalog-backend/src/database/migrations/20200520140700_location_update_log_table.ts b/plugins/catalog-backend/src/database/migrations/20200520140700_location_update_log_table.ts index b2e1dc0d32..6700f5748e 100644 --- a/plugins/catalog-backend/src/database/migrations/20200520140700_location_update_log_table.ts +++ b/plugins/catalog-backend/src/database/migrations/20200520140700_location_update_log_table.ts @@ -19,7 +19,10 @@ export async function up(knex: Knex): Promise { 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 + .dateTime('created_at') + .defaultTo(knex.fn.now()) + .notNullable(); table.string('message'); table .uuid('location_id') diff --git a/plugins/catalog-backend/src/ingestion/IngestionModels.ts b/plugins/catalog-backend/src/ingestion/IngestionModels.ts new file mode 100644 index 0000000000..616f1b49ae --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/IngestionModels.ts @@ -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. + */ + +import { EntityPolicy, EntityPolicies } from '@backstage/catalog-model'; +import { DescriptorParser, ReaderOutput } from './descriptor/parsers/types'; +import { LocationReader, LocationReaders } from './source'; +import { IngestionModel } from './types'; +import { DescriptorParsers } from './descriptor'; + +export class IngestionModels implements IngestionModel { + private readonly reader: LocationReader; + private readonly parser: DescriptorParser; + private readonly entityPolicy: EntityPolicy; + + static default(): IngestionModel { + return new IngestionModels( + new LocationReaders(), + new DescriptorParsers(), + new EntityPolicies(), + ); + } + + constructor( + reader: LocationReader, + parser: DescriptorParser, + entityPolicy: EntityPolicy, + ) { + this.reader = reader; + this.parser = parser; + this.entityPolicy = entityPolicy; + } + + async readLocation(type: string, target: string): Promise { + const buffer = await this.reader.tryRead(type, target); + if (!buffer) { + throw new Error(`No reader could handle location ${type} ${target}`); + } + + const items = await this.parser.tryParse(buffer); + if (!items) { + throw new Error(`No parser could handle location ${type} ${target}`); + } + + const result: ReaderOutput[] = []; + for (const item of items) { + if (item.type === 'error') { + result.push(item); + } else { + try { + const output = await this.entityPolicy.apply(item.data); + result.push({ type: 'data', data: output }); + } catch (e) { + result.push({ type: 'error', error: e }); + } + } + } + + return result; + } +} diff --git a/plugins/catalog-backend/src/ingestion/descriptor/DescriptorParsers.ts b/plugins/catalog-backend/src/ingestion/descriptor/DescriptorParsers.ts new file mode 100644 index 0000000000..ed05855109 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/descriptor/DescriptorParsers.ts @@ -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. + */ + +import { DescriptorParser, ReaderOutput } from './parsers/types'; +import { YamlDescriptorParser } from './parsers/YamlDescriptorParser'; + +/** + * Parses raw descriptor data (e.g. from a file or stream) into entities. + */ +export class DescriptorParsers implements DescriptorParser { + private readonly parsers: DescriptorParser[]; + + static defaultParsers(): DescriptorParser[] { + return [new YamlDescriptorParser()]; + } + + constructor( + parsers: DescriptorParser[] = DescriptorParsers.defaultParsers(), + ) { + this.parsers = parsers; + } + + async tryParse(data: Buffer): Promise { + for (const parser of this.parsers) { + const result = await parser.tryParse(data); + if (result) { + return result; + } + } + throw new Error(`Unsupported descriptor format`); + } +} diff --git a/plugins/catalog-backend/src/ingestion/descriptor/index.ts b/plugins/catalog-backend/src/ingestion/descriptor/index.ts new file mode 100644 index 0000000000..1529c78afc --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/descriptor/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { DescriptorParsers } from './DescriptorParsers'; +export { YamlDescriptorParser } from './parsers/YamlDescriptorParser'; diff --git a/plugins/catalog-backend/src/ingestion/descriptor/parsers/YamlDescriptorParser.ts b/plugins/catalog-backend/src/ingestion/descriptor/parsers/YamlDescriptorParser.ts new file mode 100644 index 0000000000..3f2e9e3c5c --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/descriptor/parsers/YamlDescriptorParser.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import yaml from 'yaml'; +import { DescriptorParser, ReaderOutput } from './types'; + +/** + * Parses descriptors on YAML format + */ +export class YamlDescriptorParser implements DescriptorParser { + async tryParse(data: Buffer): Promise { + // TODO(freben): Should perhaps first do format detection, so the parse + // failure can be emitted as a proper error instead of just as if we + // weren't handling the format at all. + let documents; + try { + documents = yaml.parseAllDocuments(data.toString('utf8')); + } catch (e) { + return undefined; + } + + const result: ReaderOutput[] = []; + + for (const document of documents) { + if (document.contents) { + if (document.errors?.length) { + result.push({ + type: 'error', + error: new Error(`Malformed YAML document, ${document.errors[0]}`), + }); + } else { + const json = document.toJSON(); + if (typeof json !== 'object' || Array.isArray(json)) { + result.push({ + type: 'error', + error: new Error(`Malformed descriptor, expected object at root`), + }); + } else { + result.push({ + type: 'data', + data: json as Entity, + }); + } + } + } + } + + return result; + } +} diff --git a/plugins/catalog-backend/src/ingestion/descriptor/parsers/types.ts b/plugins/catalog-backend/src/ingestion/descriptor/parsers/types.ts new file mode 100644 index 0000000000..baef5dc3df --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/descriptor/parsers/types.ts @@ -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. + */ + +import { Entity } from '@backstage/catalog-model'; + +export type ReaderOutput = + | { type: 'error'; error: Error } + | { type: 'data'; data: Entity }; + +/** + * Parses raw descriptor data (e.g. from a file) into entities. + */ +export type DescriptorParser = { + /** + * Try to parse some raw data into an entity. + * + * Note that this is only the low level operation of parsing the raw file + * format, e.g. reading JSON or YAML or similar and emitting as structured + * but unvalidated data. The actual validation is performed by EntityPolicy + * and KindParser. + * + * @param data Raw descriptor data + * @returns A list of raw unvalidated entities / errors, or undefined if the + * given data is not meant to be handled by this parser + * @throws An Error if the format was handled and found to not be properly + * formed + */ + tryParse(data: Buffer): Promise; +}; diff --git a/plugins/catalog-backend/src/ingestion/source/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/source/LocationReaders.ts new file mode 100644 index 0000000000..a670f309ad --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/source/LocationReaders.ts @@ -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. + */ + +import { FileLocationReader } from './readers/FileLocationReader'; +import { GitHubLocationReader } from './readers/GitHubLocationReader'; +import { LocationReader } from './readers/types'; + +export class LocationReaders implements LocationReader { + private readonly readers: LocationReader[]; + + static defaultReaders(): LocationReader[] { + return [new FileLocationReader(), new GitHubLocationReader()]; + } + + constructor(readers: LocationReader[] = LocationReaders.defaultReaders()) { + this.readers = readers; + } + + async tryRead(type: string, target: string): Promise { + for (const reader of this.readers) { + const result = await reader.tryRead(type, target); + if (result) { + return result; + } + } + throw new Error(`Could not read unknown location "${type}", "${target}"`); + } +} diff --git a/plugins/catalog-backend/src/ingestion/source/index.ts b/plugins/catalog-backend/src/ingestion/source/index.ts new file mode 100644 index 0000000000..3ed1063878 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/source/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { LocationReaders } from './LocationReaders'; +export { FileLocationReader } from './readers/FileLocationReader'; +export { GitHubLocationReader } from './readers/GitHubLocationReader'; +export { LocationReader } from './readers/types'; diff --git a/plugins/catalog-backend/src/ingestion/source/readers/FileLocationReader.ts b/plugins/catalog-backend/src/ingestion/source/readers/FileLocationReader.ts new file mode 100644 index 0000000000..0c64aebf14 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/source/readers/FileLocationReader.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { LocationReader } from './types'; + +/** + * Reads a file from the local file system. + */ +export class FileLocationReader implements LocationReader { + async tryRead(type: string, target: string): Promise { + if (type !== 'file') { + return undefined; + } + + try { + return await fs.readFile(target); + } catch (e) { + throw new Error(`Unable to read "${target}", ${e}`); + } + } +} diff --git a/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts b/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts new file mode 100644 index 0000000000..1f0f6ed539 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts @@ -0,0 +1,92 @@ +/* + * 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. + */ + +jest.mock('node-fetch'); + +import fetch from 'node-fetch'; +import { GitHubLocationReader } from './GitHubLocationReader'; + +const { Response } = jest.requireActual('node-fetch'); + +describe('Unit: GitHubLocationReader', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('fetches the file and parses it correctly', async () => { + (fetch as any).mockResolvedValueOnce(new Response('hello')); + + const reader = new GitHubLocationReader(); + const buffer = await reader.tryRead( + 'github', + 'https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/fixtures/one_component.yaml', + ); + + expect(buffer?.toString('utf8')).toBe('hello'); + }); + + it('changes the url to point to https://raw.githubusercontent.com', async () => { + const gitHubUrl = `https://github.com`; + const project = `spotify/backstage`; + const folderPath = `master/plugins/catalog-backend/fixtures`; + const componentFilename = `one_component.yaml`; + const rawGitHubUrl = `https://raw.githubusercontent.com`; + + const reader = new GitHubLocationReader(); + (fetch as any).mockResolvedValueOnce(new Response('hello')); + + await reader.tryRead( + 'github', + `${gitHubUrl}/${project}/blob/${folderPath}/${componentFilename}`, + ); + + expect(fetch).toHaveBeenCalledWith( + `${rawGitHubUrl}/${project}/${folderPath}/${componentFilename}`, + ); + }); + + describe('rejects wrong urls', () => { + const reader = new GitHubLocationReader(); + + it.each([ + ['http://example.com/one_component.yaml'], + ['http://github.com/one_component.yaml'], + ['http://github.com/PROJECT/one_component.yaml'], + ['http://github.com/PROJECT/REPO/one_component.yaml'], + ['http://github.com/PROJECT/REPO/one_component.json'], + ])( + '%p', + async (url: string) => + await expect(reader.tryRead('github', url)).rejects.toThrow(/url/), + ); + }); +}); + +describe('Integration: GitHubLocationSource', () => { + beforeAll(() => { + (fetch as any).mockImplementation(jest.requireActual('node-fetch')); + }); + + it('fetches the fixture from backstage repo', async () => { + const PERMANENT_LINK = + 'https://github.com/spotify/backstage/blob/ee84a874f8e37f87940cbe515a86c07a2db29541/plugins/catalog-backend/fixtures/one_component.yaml'; + + const reader = new GitHubLocationReader(); + const result = await reader.tryRead('github', PERMANENT_LINK); + + expect(result?.toString('utf8')).toContain('component3'); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.ts b/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.ts new file mode 100644 index 0000000000..bd330e28b4 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fetch from 'node-fetch'; +import { URL } from 'url'; +import { LocationReader } from './types'; + +/** + * Reads a file whose target is a GitHub URL. + * + * Uses raw.githubusercontent.com for now, but this will probably change in the + * future when token auth is implemented. + */ +export class GitHubLocationReader implements LocationReader { + async tryRead(type: string, target: string): Promise { + if (type !== 'github') { + return undefined; + } + + const url = this.buildRawUrl(target); + try { + return await fetch(url.toString()).then(x => x.buffer()); + } catch (e) { + throw new Error(`Unable to read "${target}", ${e}`); + } + } + + private buildRawUrl(target: string): URL { + try { + const url = new URL(target); + + const [ + empty, + userOrOrg, + repoName, + blobKeyword, + ...restOfPath + ] = url.pathname.split('/'); + + if ( + url.hostname !== 'github.com' || + empty !== '' || + userOrOrg === '' || + repoName === '' || + blobKeyword !== 'blob' || + !restOfPath.join('/').match(/\.yaml$/) + ) { + throw new Error('Wrong GitHub URL'); + } + + // Removing the "blob" part + url.pathname = [empty, userOrOrg, repoName, ...restOfPath].join('/'); + url.hostname = 'raw.githubusercontent.com'; + url.protocol = 'https'; + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } + } +} diff --git a/plugins/catalog-backend/src/ingestion/source/readers/types.ts b/plugins/catalog-backend/src/ingestion/source/readers/types.ts new file mode 100644 index 0000000000..37c7b46885 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/source/readers/types.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +export type LocationReader = { + /** + * Reads the contents of a single location. + * + * @param type The type of location to read + * @param target The location target (type-specific) + * @returns The target contents, as a raw Buffer, or undefined if this type + * was not meant to be consumed by this reader + * @throws An error if the type was meant for this reader, but could not be + * read + */ + tryRead(type: string, target: string): Promise; +}; diff --git a/yarn.lock b/yarn.lock index 6390797c12..74b9e451b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17169,11 +17169,6 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-addons-text-content@0.0.4: - version "0.0.4" - resolved "https://registry.npmjs.org/react-addons-text-content/-/react-addons-text-content-0.0.4.tgz#d2e259fdc951d1d8906c08902002108dce8792e5" - integrity sha1-0uJZ/clR0diQbAiQIAIQjc6HkuU= - react-beautiful-dnd@11.0.3: version "11.0.3" resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz#5678bb3e725d8b56cb7cf57f56e952105fc4f2af" From 1d6324a92d8fc3a835b890cdb1e44e957b3730fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 11:52:50 +0200 Subject: [PATCH 16/97] Move the model into a plugin-catalog-model for sharing outside the backend --- packages/backend/package.json | 13 +- packages/backend/src/plugins/catalog.ts | 14 +- plugins/catalog-backend/package.json | 5 +- .../src/catalog/DatabaseEntitiesCatalog.ts | 2 +- .../catalog/DatabaseLocationsCatalog.test.ts | 35 +-- .../src/catalog/DatabaseLocationsCatalog.ts | 14 +- .../src/catalog/StaticEntitiesCatalog.ts | 2 +- plugins/catalog-backend/src/catalog/types.ts | 2 +- .../src/database/Database.test.ts | 2 +- .../catalog-backend/src/database/Database.ts | 2 +- .../src/database/DatabaseManager.test.ts | 82 ++++--- .../src/database/DatabaseManager.ts | 25 +-- .../src/database/search.test.ts | 2 +- .../catalog-backend/src/database/search.ts | 2 +- plugins/catalog-backend/src/database/types.ts | 2 +- .../src/ingestion/DescriptorParsers.ts | 48 ---- .../src/ingestion/LocationReaders.ts | 39 ---- .../ComponentDescriptorV1beta1Parser.ts | 61 ----- .../DescriptorEnvelopeParser.test.ts | 172 -------------- .../descriptors/DescriptorEnvelopeParser.ts | 206 ----------------- .../catalog-backend/src/ingestion/index.ts | 7 +- .../ingestion/sources/FileLocationSource.ts | 36 --- .../ingestion/sources/GitHubLocationSource.ts | 73 ------ .../__tests__/GitHubLocationSource.test.ts | 110 --------- .../src/ingestion/sources/util.ts | 55 ----- .../catalog-backend/src/ingestion/types.ts | 168 +------------- .../src/service/router.test.ts | 2 +- plugins/catalog-backend/src/service/router.ts | 2 +- .../CommonValidatorFunctions.test.ts | 178 --------------- .../validation/CommonValidatorFunctions.ts | 108 --------- .../KubernetesValidatorFunctions.test.ts | 209 ------------------ .../KubernetesValidatorFunctions.ts | 86 ------- .../catalog-backend/src/validation/index.ts | 20 -- .../src/validation/makeValidator.ts | 38 ---- .../catalog-backend/src/validation/types.ts | 27 --- 35 files changed, 119 insertions(+), 1730 deletions(-) delete mode 100644 plugins/catalog-backend/src/ingestion/DescriptorParsers.ts delete mode 100644 plugins/catalog-backend/src/ingestion/LocationReaders.ts delete mode 100644 plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts delete mode 100644 plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.test.ts delete mode 100644 plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts delete mode 100644 plugins/catalog-backend/src/ingestion/sources/FileLocationSource.ts delete mode 100644 plugins/catalog-backend/src/ingestion/sources/GitHubLocationSource.ts delete mode 100644 plugins/catalog-backend/src/ingestion/sources/__tests__/GitHubLocationSource.test.ts delete mode 100644 plugins/catalog-backend/src/ingestion/sources/util.ts delete mode 100644 plugins/catalog-backend/src/validation/CommonValidatorFunctions.test.ts delete mode 100644 plugins/catalog-backend/src/validation/CommonValidatorFunctions.ts delete mode 100644 plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.test.ts delete mode 100644 plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.ts delete mode 100644 plugins/catalog-backend/src/validation/index.ts delete mode 100644 plugins/catalog-backend/src/validation/makeValidator.ts delete mode 100644 plugins/catalog-backend/src/validation/types.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index 9aedafecbb..51fe04e51d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -10,7 +10,7 @@ }, "scripts": { "build": "tsc", - "start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess nodemon", + "start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"nodemon -r esm\\\"", "lint": "backstage-cli lint", "test": "backstage-cli test", "clean": "backstage-cli clean", @@ -18,13 +18,15 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.6", + "@backstage/catalog-model": "^0.1.1-alpha.6", "@backstage/plugin-auth-backend": "^0.1.1-alpha.6", "@backstage/plugin-catalog-backend": "^0.1.1-alpha.6", - "@backstage/plugin-sentry-backend": "^0.1.1-alpha.6", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.6", "@backstage/plugin-identity-backend": "^0.1.1-alpha.6", + "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.6", + "@backstage/plugin-sentry-backend": "^0.1.1-alpha.6", "compression": "^1.7.4", "cors": "^2.8.5", + "esm": "^3.2.25", "express": "^4.17.1", "helmet": "^3.22.0", "knex": "^0.21.1", @@ -43,6 +45,9 @@ "typescript": "^3.9.2" }, "nodemonConfig": { - "watch": "./dist" + "watch": [ + "./dist", + "node_modules/@backstage*" + ] } } diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 687fd9157a..7e843cc80b 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -21,22 +21,28 @@ import { DatabaseManager, DescriptorParsers, LocationReaders, + IngestionModels, runPeriodically, } from '@backstage/plugin-catalog-backend'; import { PluginEnvironment } from '../types'; +import { EntityPolicies } from '@backstage/catalog-model'; export default async function ({ logger, database }: PluginEnvironment) { - const reader = LocationReaders.create(); - const parser = DescriptorParsers.create(); + const policy = new EntityPolicies(); + const ingestion = new IngestionModels( + new LocationReaders(), + new DescriptorParsers(), + new EntityPolicies(), + ); const db = await DatabaseManager.createDatabase(database, logger); runPeriodically( - () => DatabaseManager.refreshLocations(db, reader, parser, logger), + () => DatabaseManager.refreshLocations(db, ingestion, policy, logger), 10000, ); const entitiesCatalog = new DatabaseEntitiesCatalog(db); - const locationsCatalog = new DatabaseLocationsCatalog(db, reader); + const locationsCatalog = new DatabaseLocationsCatalog(db, ingestion); return await createRouter({ entitiesCatalog, locationsCatalog, logger }); } diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index f32ffffb6c..5ec37295ed 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -16,8 +16,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.6", - "@types/node-fetch": "^2.5.7", - "@types/supertest": "^2.0.8", + "@backstage/catalog-model": "^0.1.1-alpha.6", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", @@ -38,6 +37,8 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", "@types/lodash": "^4.14.151", + "@types/node-fetch": "^2.5.7", + "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", "@types/yup": "^0.28.2", "jest-fetch-mock": "^3.0.3", diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index d14b9df658..972410a639 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import { Database } from '../database'; -import { Entity } from '../ingestion/types'; import { EntitiesCatalog, EntityFilters } from './types'; export class DatabaseEntitiesCatalog implements EntitiesCatalog { diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts index a63dd7d497..56a3b3828f 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -14,12 +14,27 @@ * limitations under the License. */ import { getVoidLogger } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; import knex from 'knex'; import path from 'path'; import { Database } from '../database'; -import { ReaderOutput } from '../ingestion/types'; +import { IngestionModel } from '../ingestion/types'; import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; +class MockIngestionModel implements IngestionModel { + readLocation = jest.fn(async (type: string, target: string) => { + if (type !== 'valid_type') { + throw new Error(`Unknown location type ${type}`); + } + if (target === 'valid_target') { + return [{ type: 'data', data: {} as Entity } as const]; + } + throw new Error( + `Can't read location at ${target} with error: Something is broken`, + ); + }); +} + describe('DatabaseLocationsCatalog', () => { const database = knex({ client: 'sqlite3', @@ -31,20 +46,7 @@ describe('DatabaseLocationsCatalog', () => { }); let db: Database; let catalog: DatabaseLocationsCatalog; - - const mockLocationReader = { - read: async (type: string, target: string): Promise => { - if (type !== 'valid_type') { - throw new Error(`Unknown location type ${type}`); - } - if (target === 'valid_target') { - return Promise.resolve([{ type: 'data', data: {} }]); - } - throw new Error( - `Can't read location at ${target} with error: Something is broken`, - ); - }, - }; + let ingestionModel: IngestionModel; beforeEach(async () => { await database.migrate.latest({ @@ -52,7 +54,8 @@ describe('DatabaseLocationsCatalog', () => { loadExtensions: ['.ts'], }); db = new Database(database, getVoidLogger()); - catalog = new DatabaseLocationsCatalog(db, mockLocationReader); + ingestionModel = new MockIngestionModel(); + catalog = new DatabaseLocationsCatalog(db, ingestionModel); }); it('resolves to location with id', async () => { diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index d82d8e2c26..b13d70de86 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -15,17 +15,25 @@ */ import { Database } from '../database'; -import { LocationReader } from '../ingestion'; +import { IngestionModel } from '../ingestion/types'; import { AddLocation, Location, LocationsCatalog } from './types'; export class DatabaseLocationsCatalog implements LocationsCatalog { constructor( private readonly database: Database, - private readonly reader: LocationReader, + private readonly ingestionModel: IngestionModel, ) {} async addLocation(location: AddLocation): Promise { - const outputs = await this.reader.read(location.type, location.target); + const outputs = await this.ingestionModel.readLocation( + location.type, + location.target, + ); + if (!outputs) { + throw new Error( + `Unknown location type ${location.type} ${location.target}`, + ); + } outputs.forEach(output => { if (output.type === 'error') { throw new Error( diff --git a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts index 64cee69dd7..1de606d44d 100644 --- a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts @@ -15,8 +15,8 @@ */ import { NotFoundError } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; import lodash from 'lodash'; -import { Entity } from '../ingestion'; import { EntitiesCatalog } from './types'; export class StaticEntitiesCatalog implements EntitiesCatalog { diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 90bb6943b3..2f8967cd1b 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import * as yup from 'yup'; -import { Entity } from '../ingestion'; // // Entities diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index 18481455ea..7a38adf02f 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -19,9 +19,9 @@ import { getVoidLogger, NotFoundError, } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; import Knex from 'knex'; import path from 'path'; -import { Entity } from '../ingestion'; import { Database } from './Database'; import { AddDatabaseLocation, diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index b7777d3ccb..1f5cc7562f 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -19,12 +19,12 @@ import { InputError, NotFoundError, } from '@backstage/backend-common'; +import { Entity, EntityMeta } from '@backstage/catalog-model'; import Knex from 'knex'; import lodash from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from 'winston'; import { EntityFilters } from '../catalog'; -import { Entity, EntityMeta } from '../ingestion'; import { buildEntitySearch } from './search'; import { AddDatabaseLocation, diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts index ca50f518cb..c8d0aec332 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.test.ts @@ -15,13 +15,9 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { Entity, EntityPolicy } from '@backstage/catalog-model'; import Knex from 'knex'; -import { - ComponentDescriptor, - DescriptorParser, - LocationReader, - ParserError, -} from '../ingestion'; +import { IngestionModel } from '../ingestion/types'; import { Database } from './Database'; import { DatabaseManager } from './DatabaseManager'; import { DatabaseLocationUpdateLogStatus, DbLocationsRow } from './types'; @@ -32,18 +28,18 @@ describe('DatabaseManager', () => { const db = ({ locations: jest.fn().mockResolvedValue([]), } as unknown) as Database; - const reader: LocationReader = { - read: jest.fn(), + const reader: IngestionModel = { + readLocation: jest.fn(), }; - const parser: DescriptorParser = { - parse: jest.fn(), + const policy: EntityPolicy = { + apply: jest.fn(), }; await expect( - DatabaseManager.refreshLocations(db, reader, parser, getVoidLogger()), + DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); - expect(reader.read).not.toHaveBeenCalled(); - expect(parser.parse).not.toHaveBeenCalled(); + expect(reader.readLocation).not.toHaveBeenCalled(); + expect(policy.apply).not.toHaveBeenCalled(); }); it('can update a single location', async () => { @@ -52,7 +48,7 @@ describe('DatabaseManager', () => { type: 'some', target: 'thing', }; - const desc: ComponentDescriptor = { + const desc: Entity = { apiVersion: 'backstage.io/v1beta1', kind: 'Component', metadata: { name: 'c1' }, @@ -68,18 +64,20 @@ describe('DatabaseManager', () => { addLocationUpdateLogEvent: jest.fn(), } as Partial) as Database; - const reader: LocationReader = { - read: jest.fn(() => Promise.resolve([{ type: 'data', data: desc }])), + const reader: IngestionModel = { + readLocation: jest.fn(() => + Promise.resolve([{ type: 'data', data: desc }]), + ), }; - const parser: DescriptorParser = { - parse: jest.fn(() => Promise.resolve(desc)), + const policy: EntityPolicy = { + apply: jest.fn(() => Promise.resolve(desc)), }; await expect( - DatabaseManager.refreshLocations(db, reader, parser, getVoidLogger()), + DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); - expect(reader.read).toHaveBeenCalledTimes(1); - expect(reader.read).toHaveBeenNthCalledWith(1, 'some', 'thing'); + expect(reader.readLocation).toHaveBeenCalledTimes(1); + expect(reader.readLocation).toHaveBeenNthCalledWith(1, 'some', 'thing'); expect(db.addEntity).toHaveBeenCalledTimes(1); expect(db.addEntity).toHaveBeenNthCalledWith(1, undefined, { locationId: '123', @@ -108,21 +106,23 @@ describe('DatabaseManager', () => { addLocationUpdateLogEvent: jest.fn(), } as unknown) as Database; - const desc: ComponentDescriptor = { + const desc: Entity = { apiVersion: 'backstage.io/v1beta1', kind: 'Component', metadata: { name: 'c1' }, spec: { type: 'service' }, }; - const reader: LocationReader = { - read: jest.fn(() => Promise.resolve([{ type: 'data', data: desc }])), + const reader: IngestionModel = { + readLocation: jest.fn(() => + Promise.resolve([{ type: 'data', data: desc }]), + ), }; - const parser: DescriptorParser = { - parse: jest.fn(() => Promise.resolve(desc)), + const policy: EntityPolicy = { + apply: jest.fn(() => Promise.resolve(desc)), }; await expect( - DatabaseManager.refreshLocations(db, reader, parser, getVoidLogger()), + DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( @@ -158,23 +158,23 @@ describe('DatabaseManager', () => { addLocationUpdateLogEvent: jest.fn(), } as unknown) as Database; - const desc: ComponentDescriptor = { + const desc: Entity = { apiVersion: 'backstage.io/v1beta1', kind: 'Component', metadata: { name: 'c1' }, spec: { type: 'service' }, }; - const reader: LocationReader = { - read: jest.fn(() => Promise.resolve([{ type: 'data', data: desc }])), - }; - const parser: DescriptorParser = { - parse: jest.fn(() => - Promise.reject(new ParserError('parser error message', 'c1')), + const reader: IngestionModel = { + readLocation: jest.fn(() => + Promise.resolve([{ type: 'data', data: desc }]), ), }; + const policy: EntityPolicy = { + apply: jest.fn(() => Promise.reject(new Error('parser error message'))), + }; await expect( - DatabaseManager.refreshLocations(db, reader, parser, getVoidLogger()), + DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( @@ -211,19 +211,17 @@ describe('DatabaseManager', () => { addLocationUpdateLogEvent: jest.fn(), } as unknown) as Database; - const reader: LocationReader = { - read: jest.fn(() => + const reader: IngestionModel = { + readLocation: jest.fn(() => Promise.reject([{ type: 'error', error: new Error('test message') }]), ), }; - const parser: DescriptorParser = { - parse: jest.fn(() => - Promise.reject(new ParserError('parser error message', 'c1')), - ), + const policy: EntityPolicy = { + apply: jest.fn(() => Promise.reject(new Error('parser error message'))), }; await expect( - DatabaseManager.refreshLocations(db, reader, parser, getVoidLogger()), + DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index 4b7fa38326..a3aded18ec 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -14,16 +14,12 @@ * limitations under the License. */ +import { Entity, EntityPolicy } from '@backstage/catalog-model'; import Knex from 'knex'; import lodash from 'lodash'; import path from 'path'; import { Logger } from 'winston'; -import { - DescriptorParser, - Entity, - LocationReader, - ParserError, -} from '../ingestion'; +import { IngestionModel } from '../ingestion/types'; import { Database } from './Database'; import { DatabaseLocationUpdateLogStatus, DbEntityRequest } from './types'; @@ -67,8 +63,8 @@ export class DatabaseManager { public static async refreshLocations( database: Database, - reader: LocationReader, - parser: DescriptorParser, + ingestionModel: IngestionModel, + entityPolicy: EntityPolicy, logger: Logger, ): Promise { const locations = await database.locations(); @@ -78,7 +74,10 @@ export class DatabaseManager { `Refreshing location id="${location.id}" type="${location.type}" target="${location.target}"`, ); - const readerOutput = await reader.read(location.type, location.target); + const readerOutput = await ingestionModel.readLocation( + location.type, + location.target, + ); for (const readerItem of readerOutput) { if (readerItem.type === 'error') { @@ -87,7 +86,7 @@ export class DatabaseManager { } try { - const entity = await parser.parse(readerItem.data); + const entity = await entityPolicy.apply(readerItem.data); await DatabaseManager.refreshSingleEntity( database, location.id, @@ -100,15 +99,11 @@ export class DatabaseManager { entity.metadata!.name, ); } catch (error) { - let entityName; - if (error instanceof ParserError) { - entityName = error.entityName; - } await DatabaseManager.logUpdateFailure( database, location.id, error, - entityName, + readerItem.data.metadata?.name, ); } } diff --git a/plugins/catalog-backend/src/database/search.test.ts b/plugins/catalog-backend/src/database/search.test.ts index 4c1df3429e..7ad06aee14 100644 --- a/plugins/catalog-backend/src/database/search.test.ts +++ b/plugins/catalog-backend/src/database/search.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity } from '../ingestion'; +import { Entity } from '@backstage/catalog-model'; import { buildEntitySearch, visitEntityPart } from './search'; import { DbEntitiesSearchRow } from './types'; diff --git a/plugins/catalog-backend/src/database/search.ts b/plugins/catalog-backend/src/database/search.ts index e14ebd338f..fcacf1a9f2 100644 --- a/plugins/catalog-backend/src/database/search.ts +++ b/plugins/catalog-backend/src/database/search.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity } from '../ingestion'; +import { Entity } from '@backstage/catalog-model'; import { DbEntitiesSearchRow } from './types'; // Search entries that start with these prefixes, also get a shorthand without diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 9fe4a523cc..ca1cbbdfd4 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import * as yup from 'yup'; -import { Entity } from '../ingestion'; export type DbEntitiesRow = { id: string; diff --git a/plugins/catalog-backend/src/ingestion/DescriptorParsers.ts b/plugins/catalog-backend/src/ingestion/DescriptorParsers.ts deleted file mode 100644 index 6375351ebd..0000000000 --- a/plugins/catalog-backend/src/ingestion/DescriptorParsers.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { makeValidator } from '../validation'; -import { ComponentDescriptorV1beta1Parser } from './descriptors/ComponentDescriptorV1beta1Parser'; -import { DescriptorEnvelopeParser } from './descriptors/DescriptorEnvelopeParser'; -import { DescriptorParser, Entity, KindParser, ParserError } from './types'; - -export class DescriptorParsers implements DescriptorParser { - static create(): DescriptorParser { - const validators = makeValidator(); - return new DescriptorParsers(new DescriptorEnvelopeParser(validators), [ - new ComponentDescriptorV1beta1Parser(), - ]); - } - - constructor( - private readonly envelopeParser: DescriptorEnvelopeParser, - private readonly kindParsers: KindParser[], - ) {} - - async parse(descriptor: object): Promise { - const envelope = await this.envelopeParser.parse(descriptor); - for (const parser of this.kindParsers) { - const parsed = await parser.tryParse(envelope); - if (parsed) { - return parsed; - } - } - throw new ParserError( - `Unsupported object ${envelope.apiVersion}, ${envelope.kind}`, - envelope.metadata?.name, - ); - } -} diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts deleted file mode 100644 index aa88d299e4..0000000000 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FileLocationSource } from './sources/FileLocationSource'; -import { GitHubLocationSource } from './sources/GitHubLocationSource'; -import { LocationReader, LocationSource, ReaderOutput } from './types'; - -export class LocationReaders implements LocationReader { - static create(): LocationReader { - return new LocationReaders({ - file: new FileLocationSource(), - github: new GitHubLocationSource(), - }); - } - - constructor(private readonly sources: Record) {} - - async read(type: string, target: string): Promise { - const source = this.sources[type]; - if (!source) { - throw new Error(`Unknown location type ${type}`); - } - - return source.read(target); - } -} diff --git a/plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts b/plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts deleted file mode 100644 index 0934231a0c..0000000000 --- a/plugins/catalog-backend/src/ingestion/descriptors/ComponentDescriptorV1beta1Parser.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as yup from 'yup'; -import { Entity, KindParser, ParserError } from '../types'; - -export interface ComponentDescriptorV1beta1 extends Entity { - spec: { - type: string; - }; -} - -export class ComponentDescriptorV1beta1Parser implements KindParser { - private schema: yup.Schema; - - constructor() { - this.schema = yup.object>({ - metadata: yup - .object({ - name: yup.string().required(), - }) - .required(), - spec: yup - .object({ - type: yup.string().required(), - }) - .required(), - }); - } - - async tryParse(envelope: Entity): Promise { - if ( - envelope.apiVersion !== 'backstage.io/v1beta1' || - envelope.kind !== 'Component' - ) { - return undefined; - } - - try { - return await this.schema.validate(envelope, { strict: true }); - } catch (e) { - throw new ParserError( - `Malformed component, ${e}`, - envelope.metadata?.name, - ); - } - } -} diff --git a/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.test.ts b/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.test.ts deleted file mode 100644 index 7c96fb7cf5..0000000000 --- a/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import yaml from 'yaml'; -import { makeValidator } from '../../validation'; -import { DescriptorEnvelopeParser } from './DescriptorEnvelopeParser'; - -describe('DescriptorEnvelopeParser', () => { - let data: any; - let parser: DescriptorEnvelopeParser; - - beforeEach(() => { - data = yaml.parse(` - apiVersion: backstage.io/v1beta1 - kind: Component - metadata: - uid: e01199ab-08cc-44c2-8e19-5c29ded82521 - etag: lsndfkjsndfkjnsdfkjnsd== - generation: 13 - name: my-component-yay - namespace: the-namespace - labels: - backstage.io/custom: ValueStuff - annotations: - example.com/bindings: are-secret - spec: - custom: stuff - `); - parser = new DescriptorEnvelopeParser(makeValidator()); - }); - - it('works for the happy path', async () => { - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects missing apiVersion', async () => { - delete data.apiVersion; - await expect(parser.parse(data)).rejects.toThrow(/apiVersion/); - }); - - it('rejects wrong root type', async () => { - await expect(parser.parse(7)).rejects.toThrow(/object/); - }); - - it('rejects bad apiVersion', async () => { - data.apiVersion = 'a#b'; - await expect(parser.parse(data)).rejects.toThrow(/apiVersion/); - }); - - it('rejects missing kind', async () => { - delete data.kind; - await expect(parser.parse(data)).rejects.toThrow(/kind/); - }); - - it('rejects bad kind', async () => { - data.kind = 'a#b'; - await expect(parser.parse(data)).rejects.toThrow(/kind/); - }); - - it('accepts missing metadata', async () => { - delete data.medatata; - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects non-object metadata', async () => { - data.metadata = 7; - await expect(parser.parse(data)).rejects.toThrow(/metadata/); - }); - - it('accepts missing uid', async () => { - delete data.metadata.uid; - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects bad uid', async () => { - data.metadata.uid = 7; - await expect(parser.parse(data)).rejects.toThrow(/uid/); - }); - - it('accepts missing etag', async () => { - delete data.metadata.etag; - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects bad etag', async () => { - data.metadata.etag = 7; - await expect(parser.parse(data)).rejects.toThrow(/etag/); - }); - - it('accepts missing generation', async () => { - delete data.metadata.generation; - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects bad generation', async () => { - data.metadata.generation = 'a'; - await expect(parser.parse(data)).rejects.toThrow(/generation/); - }); - - it('accepts missing spec', async () => { - delete data.spec; - await expect(parser.parse(data)).resolves.toBe(data); - }); - - it('rejects non-object spec', async () => { - data.spec = 7; - await expect(parser.parse(data)).rejects.toThrow(/spec/); - }); - - it('rejects bad name', async () => { - data.metadata.name = 7; - await expect(parser.parse(data)).rejects.toThrow(/name/); - }); - - it('rejects bad namespace', async () => { - data.metadata.namespace = 7; - await expect(parser.parse(data)).rejects.toThrow(/namespace/); - }); - - it('rejects bad label key', async () => { - data.metadata.labels['a#b'] = 'value'; - await expect(parser.parse(data)).rejects.toThrow(/label.*key/i); - }); - - it('rejects bad label value', async () => { - data.metadata.labels.a = 'a#b'; - await expect(parser.parse(data)).rejects.toThrow(/label.*value/i); - }); - - it('rejects bad annotation key', async () => { - data.metadata.annotations['a#b'] = 'value'; - await expect(parser.parse(data)).rejects.toThrow(/annotation.*key/i); - }); - - it('rejects bad annotation value', async () => { - data.metadata.annotations.a = []; - await expect(parser.parse(data)).rejects.toThrow(/annotation.*value/i); - }); - - it('rejects unknown root keys', async () => { - data.spec2 = {}; - await expect(parser.parse(data)).rejects.toThrow(/spec2/i); - }); - - it('rejects reserved keys in the spec root', async () => { - data.spec.apiVersion = 'a/b'; - await expect(parser.parse(data)).rejects.toThrow(/spec.*apiVersion/i); - }); - - it('rejects reserved keys in labels', async () => { - data.metadata.labels.apiVersion = 'a'; - await expect(parser.parse(data)).rejects.toThrow(/label.*apiVersion/i); - }); - - it('rejects reserved keys in annotations', async () => { - data.metadata.annotations.apiVersion = 'a'; - await expect(parser.parse(data)).rejects.toThrow(/annotation.*apiVersion/i); - }); -}); diff --git a/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts b/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts deleted file mode 100644 index 3833d22e77..0000000000 --- a/plugins/catalog-backend/src/ingestion/descriptors/DescriptorEnvelopeParser.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as yup from 'yup'; -import { Validators } from '../../validation'; -import { Entity } from '../types'; - -/** - * Parses some raw structured data as a descriptor envelope - */ -export class DescriptorEnvelopeParser { - private schema: yup.Schema; - - constructor(validators: Validators) { - const apiVersionSchema = yup - .string() - .required() - .test( - 'apiVersion', - 'The apiVersion is not formatted according to schema', - validators.isValidApiVersion, - ); - - const kindSchema = yup - .string() - .required() - .test( - 'kind', - 'The kind is not formatted according to schema', - validators.isValidKind, - ); - - const uidSchema = yup - .string() - .notRequired() - .test( - 'metadata.uid', - 'The uid is not formatted according to schema', - value => value === undefined || value.length > 0, - ); - - const etagSchema = yup - .string() - .notRequired() - .test( - 'metadata.etag', - 'The etag value is not according to schema', - value => value === undefined || value.length > 0, - ); - - const generationSchema = yup - .number() - .notRequired() - .test( - 'metadata.generation', - 'The generation value is not according to schema', - value => value === undefined || value > 0, - ); - - const nameSchema = yup - .string() - .notRequired() - .test( - 'metadata.name', - 'The name is not formatted according to schema', - value => value === undefined || validators.isValidEntityName(value), - ); - - const namespaceSchema = yup - .string() - .notRequired() - .test( - 'metadata.namespace', - 'The namespace is malformed', - value => value === undefined || validators.isValidNamespace(value), - ); - - const labelsSchema = yup - .object>() - .notRequired() - .test({ - name: 'metadata.labels.keys', - message: 'Label keys not formatted according to schema', - test(value: object) { - return ( - value === undefined || - Object.keys(value).every(validators.isValidLabelKey) - ); - }, - }) - .test({ - name: 'metadata.labels.values', - message: 'Label values not formatted according to schema', - test(value: object) { - return ( - value === undefined || - Object.values(value).every(validators.isValidLabelValue) - ); - }, - }); - - const annotationsSchema = yup - .object>() - .notRequired() - .test({ - name: 'metadata.annotations.keys', - message: 'Annotation keys not formatted according to schema', - test(value: object) { - return ( - value === undefined || - Object.keys(value).every(validators.isValidAnnotationKey) - ); - }, - }) - .test({ - name: 'metadata.annotations.values', - message: 'Annotation values not formatted according to schema', - test(value: object) { - return ( - value === undefined || - Object.values(value).every(validators.isValidAnnotationValue) - ); - }, - }); - - const metadataSchema = yup - .object({ - uid: uidSchema, - etag: etagSchema, - generation: generationSchema, - name: nameSchema, - namespace: namespaceSchema, - labels: labelsSchema, - annotations: annotationsSchema, - }) - .notRequired(); - - const specSchema = yup.object({}).notRequired(); - - this.schema = yup - .object({ - apiVersion: apiVersionSchema, - kind: kindSchema, - metadata: metadataSchema, - spec: specSchema, - }) - .noUnknown(); - } - - async parse(data: any): Promise { - let result: Entity; - try { - result = await this.schema.validate(data, { strict: true }); - } catch (e) { - throw new Error(`Malformed envelope, ${e}`); - } - - // These are keys with specific semantic meaning in a document, that we do - // not want to appear in the root of the spec, or as labels or as - // annotations, because they will lead to confusion. - const reservedKeys = [ - 'apiVersion', - 'kind', - 'uid', - 'etag', - 'generation', - 'name', - 'namespace', - 'labels', - 'annotations', - 'spec', - ]; - for (const key of reservedKeys) { - if (result.spec?.hasOwnProperty(key)) { - throw new Error( - `The spec may not contain the key ${key}, because it has reserved meaning`, - ); - } - if (result.metadata?.labels?.hasOwnProperty(key)) { - throw new Error( - `A label may not have the key ${key}, because it has reserved meaning`, - ); - } - if (result.metadata?.annotations?.hasOwnProperty(key)) { - throw new Error( - `An annotation may not have the key ${key}, because it has reserved meaning`, - ); - } - } - - return result; - } -} diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index ca6f2dd2be..b6aceaecdd 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ -export * from './DescriptorParsers'; -export * from './LocationReaders'; -export * from './types'; +export * from './descriptor'; +export { IngestionModels } from './IngestionModels'; +export * from './source'; +export type { IngestionModel } from './types'; diff --git a/plugins/catalog-backend/src/ingestion/sources/FileLocationSource.ts b/plugins/catalog-backend/src/ingestion/sources/FileLocationSource.ts deleted file mode 100644 index 9d2794ec4f..0000000000 --- a/plugins/catalog-backend/src/ingestion/sources/FileLocationSource.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs-extra'; -import { LocationSource, ReaderOutput } from '../types'; -import { readDescriptorYaml } from './util'; - -export class FileLocationSource implements LocationSource { - async read(target: string): Promise { - let rawYaml; - try { - rawYaml = await fs.readFile(target, 'utf8'); - } catch (e) { - throw new Error(`Unable to read "${target}", ${e}`); - } - - try { - return readDescriptorYaml(rawYaml); - } catch (e) { - throw new Error(`Malformed descriptor at "${target}", ${e}`); - } - } -} diff --git a/plugins/catalog-backend/src/ingestion/sources/GitHubLocationSource.ts b/plugins/catalog-backend/src/ingestion/sources/GitHubLocationSource.ts deleted file mode 100644 index 69ba4911a6..0000000000 --- a/plugins/catalog-backend/src/ingestion/sources/GitHubLocationSource.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fetch from 'node-fetch'; -import { URL } from 'url'; -import { LocationSource, ReaderOutput } from '../types'; -import { readDescriptorYaml } from './util'; - -// Pointing to raw.githubusercontent.com for now -// to be changed in the future, after auth and tokens are done -export class GitHubLocationSource implements LocationSource { - async read(target: string): Promise { - let url: URL; - - try { - url = new URL(target); - - const [ - empty, - userOrOrg, - repoName, - blobKeyword, - ...restOfPath - ] = url.pathname.split('/'); - - if ( - url.hostname !== 'github.com' || - empty !== '' || - userOrOrg === '' || - repoName === '' || - blobKeyword !== 'blob' || - !restOfPath.join('/').match(/\.yaml$/) - ) { - throw new Error('Wrong GitHub URL'); - } - - // Removing the "blob" part - url.pathname = [empty, userOrOrg, repoName, ...restOfPath].join('/'); - url.hostname = 'raw.githubusercontent.com'; - url.protocol = 'https'; - } catch (e) { - throw new Error(`Incorrect url: ${target}, ${e}`); - } - - let rawYaml; - try { - rawYaml = await fetch(url.toString()).then(x => { - return x.text(); - }); - } catch (e) { - throw new Error(`Unable to read "${target}", ${e}`); - } - - try { - return readDescriptorYaml(rawYaml); - } catch (e) { - throw new Error(`Malformed descriptor at "${target}", ${e}`); - } - } -} diff --git a/plugins/catalog-backend/src/ingestion/sources/__tests__/GitHubLocationSource.test.ts b/plugins/catalog-backend/src/ingestion/sources/__tests__/GitHubLocationSource.test.ts deleted file mode 100644 index 083c2f7e86..0000000000 --- a/plugins/catalog-backend/src/ingestion/sources/__tests__/GitHubLocationSource.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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. - */ - -jest.mock('node-fetch'); - -import fs from 'fs-extra'; -import fetch from 'node-fetch'; -import path from 'path'; -import { GitHubLocationSource } from '../GitHubLocationSource'; - -const { Response } = jest.requireActual('node-fetch'); - -const FIXTURES_DIR = path.resolve( - __dirname, - '..', - '..', - '..', - '..', - 'fixtures', -); -const fixtures = fs.readdirSync(FIXTURES_DIR).reduce((acc, filename) => { - acc[filename] = fs.readFileSync(path.resolve(FIXTURES_DIR, filename), 'utf8'); - return acc; -}, {} as Record); - -describe('Unit: GitHubLocationSource', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('fetches the file and parses it correctly', async () => { - (fetch as any).mockReturnValueOnce( - Promise.resolve(new Response(fixtures['one_component.yaml'])), - ); - const reader = new GitHubLocationSource(); - - const result = await reader.read( - 'https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/fixtures/one_component.yaml', - ); - - expect(result[0].type).toBe('data'); - expect((result[0] as any).data.metadata.name).toBe('component3'); - }); - - it('changes the url to point to https://raw.githubusercontent.com', async () => { - const gitHubUrl = `https://github.com`; - const project = `spotify/backstage`; - const folderPath = `master/plugins/catalog-backend/fixtures`; - const componentFilename = `one_component.yaml`; - const rawGitHubUrl = `https://raw.githubusercontent.com`; - const reader = new GitHubLocationSource(); - (fetch as any).mockReturnValueOnce( - Promise.resolve(new Response(fixtures[componentFilename])), - ); - - await reader.read( - `${gitHubUrl}/${project}/blob/${folderPath}/${componentFilename}`, - ); - - expect(fetch).toHaveBeenCalledWith( - `${rawGitHubUrl}/${project}/${folderPath}/${componentFilename}`, - ); - }); - - describe('rejects wrong urls', () => { - const reader = new GitHubLocationSource(); - - it.each([ - ['http://example.com/one_component.yaml'], - ['http://github.com/one_component.yaml'], - ['http://github.com/PROJECT/one_component.yaml'], - ['http://github.com/PROJECT/REPO/one_component.yaml'], - ['http://github.com/PROJECT/REPO/one_component.json'], - ])( - '%p', - async (url: string) => - await expect(reader.read(url)).rejects.toThrow(/url/), - ); - }); -}); - -describe('Integration: GitHubLocationSource', () => { - beforeAll(() => { - (fetch as any).mockImplementation(jest.requireActual('node-fetch')); - }); - - it('fetches the fixture from backstage repo', async () => { - const PERMANENT_LINK = - 'https://github.com/spotify/backstage/blob/ee84a874f8e37f87940cbe515a86c07a2db29541/plugins/catalog-backend/fixtures/one_component.yaml'; - const reader = new GitHubLocationSource(); - - const result = await reader.read(PERMANENT_LINK); - - expect(result[0].type).toBe('data'); - expect((result[0] as any).data.metadata.name).toBe('component3'); - }); -}); diff --git a/plugins/catalog-backend/src/ingestion/sources/util.ts b/plugins/catalog-backend/src/ingestion/sources/util.ts deleted file mode 100644 index cccbeb92b3..0000000000 --- a/plugins/catalog-backend/src/ingestion/sources/util.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import yaml from 'yaml'; -import { ReaderOutput } from '../types'; - -export function readDescriptorYaml(data: string): ReaderOutput[] { - let documents; - try { - documents = yaml.parseAllDocuments(data); - } catch (e) { - throw new Error(`Could not parse YAML data, ${e}`); - } - - const result: ReaderOutput[] = []; - - for (const document of documents) { - if (document.contents) { - if (document.errors?.length) { - result.push({ - type: 'error', - error: new Error(`Malformed YAML document, ${document.errors[0]}`), - }); - } else { - const json = document.toJSON(); - if (typeof json !== 'object' || Array.isArray(json)) { - result.push({ - type: 'error', - error: new Error(`Malformed descriptor, expected object at root`), - }); - } else { - result.push({ - type: 'data', - data: json, - }); - } - } - } - } - - return result; -} diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index 45ee3bc638..8878c2af5b 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -14,170 +14,8 @@ * limitations under the License. */ -import { ComponentDescriptorV1beta1 } from './descriptors/ComponentDescriptorV1beta1Parser'; +import { ReaderOutput } from './descriptor/parsers/types'; -export type ComponentDescriptor = ComponentDescriptorV1beta1; - -/** - * Metadata fields common to all versions/kinds of entity. - * - * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta - */ -export type EntityMeta = { - /** - * A globally unique ID for the entity. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. The field can (optionally) be specified when performing - * update or delete operations, but the server is free to reject requests - * that do so in such a way that it breaks semantics. - */ - uid?: string; - - /** - * An opaque string that changes for each update operation to any part of - * the entity, including metadata. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. The field can (optionally) be specified when performing - * update or delete operations, and the server will then reject the - * operation if it does not match the current stored value. - */ - etag?: string; - - /** - * A positive nonzero number that indicates the current generation of data - * for this entity; the value is incremented each time the spec changes. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. - */ - generation?: number; - - /** - * The name of the entity. - * - * Must be uniqe within the catalog at any given point in time, for any - * given namespace, for any given kind. - */ - name?: string; - - /** - * The namespace that the entity belongs to. - */ - namespace?: string; - - /** - * Key/value pairs of identifying information attached to the entity. - */ - labels?: Record; - - /** - * Key/value pairs of non-identifying auxiliary information attached to the - * entity. - */ - annotations?: Record; -}; - -/** - * The format envelope that's common to all versions/kinds. - * - * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ - */ -export type Entity = { - /** - * The version of specification format for this particular entity that - * this is written against. - */ - apiVersion: string; - - /** - * The high level entity type being described. - */ - kind: string; - - /** - * Optional metadata related to the entity. - */ - metadata?: EntityMeta; - - /** - * The specification data describing the entity itself. - */ - spec?: object; -}; - -/** - * Parses and validates descriptors. - * - * The output must be validated and well formed. - */ -export type DescriptorParser = { - /** - * Parses and validates a single raw descriptor. - * - * @param descriptor A raw descriptor object - * @returns A structure describing the parsed and validated descriptor - * @throws An Error if the descriptor was malformed - */ - parse(descriptor: object): Promise; -}; - -/** - * Parses and validates a single envelope into its materialized kind. - * - * These parsers may assume that the envelope is already validated and well - * formed. - */ -export type KindParser = { - /** - * Try to parse an envelope into a materialized kind. - * - * @param envelope A valid descriptor envelope - * @returns A materialized type, or undefined if the given version/kind is - * not meant to be handled by this parser - * @throws An Error if the type was handled and found to not be properly - * formatted - */ - tryParse(envelope: Entity): Promise; -}; - -export class ParserError extends Error { - constructor(message?: string, private _entityName?: string | undefined) { - super(message); - } - get entityName() { - return this._entityName; - } -} - -export type ReaderOutput = - | { type: 'error'; error: Error } - | { type: 'data'; data: object }; - -export type LocationReader = { - /** - * Reads the contents of a single location. - * - * @param type The type of location to read - * @param target The location target (type-specific) - * @returns The parsed contents, as an array of unverified descriptors or - * errors where the individual documents could not be parsed. - * @throws An error if the location as a whole could not be read - */ - read(type: string, target: string): Promise; -}; - -export type LocationSource = { - /** - * Reads the contents of a single location. - * - * @param target The location target to read - * @returns The parsed contents, as an array of unverified descriptors - * @throws An error if the location target could not be read - */ - read(target: string): Promise; +export type IngestionModel = { + readLocation(type: string, target: string): Promise; }; diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 210ad98577..36e8b55770 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -15,10 +15,10 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; import request from 'supertest'; import { EntitiesCatalog, Location, LocationsCatalog } from '../catalog'; -import { Entity } from '../ingestion'; import { createRouter } from './router'; class MockEntitiesCatalog implements EntitiesCatalog { diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index c3318a7377..0678cd3aef 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -22,7 +22,7 @@ import { addLocationSchema, EntitiesCatalog, EntityFilters, - LocationsCatalog, + LocationsCatalog } from '../catalog'; import { validateRequestBody } from './util'; diff --git a/plugins/catalog-backend/src/validation/CommonValidatorFunctions.test.ts b/plugins/catalog-backend/src/validation/CommonValidatorFunctions.test.ts deleted file mode 100644 index 200e90b406..0000000000 --- a/plugins/catalog-backend/src/validation/CommonValidatorFunctions.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CommonValidatorFunctions } from './CommonValidatorFunctions'; - -describe('CommonValidatorFunctions', () => { - describe('isValidPrefixAndOrSuffix', () => { - it('only accepts strings', () => { - expect( - CommonValidatorFunctions.isValidPrefixAndOrSuffix( - null, - '/', - () => true, - () => true, - ), - ).toBe(false); - expect( - CommonValidatorFunctions.isValidPrefixAndOrSuffix( - 7, - '/', - () => true, - () => true, - ), - ).toBe(false); - expect( - CommonValidatorFunctions.isValidPrefixAndOrSuffix( - () => 'hello', - '/', - () => true, - () => true, - ), - ).toBe(false); - }); - - it('only accepts one or two parts', () => { - expect( - CommonValidatorFunctions.isValidPrefixAndOrSuffix( - 'a', - '/', - () => true, - () => true, - ), - ).toBe(true); - expect( - CommonValidatorFunctions.isValidPrefixAndOrSuffix( - 'a/b', - '/', - () => true, - () => true, - ), - ).toBe(true); - expect( - CommonValidatorFunctions.isValidPrefixAndOrSuffix( - 'a/b/c', - '/', - () => true, - () => true, - ), - ).toBe(false); - }); - - it('checks the prefix and suffix', () => { - expect( - CommonValidatorFunctions.isValidPrefixAndOrSuffix( - 'a/b', - '/', - () => true, - () => true, - ), - ).toBe(true); - expect( - CommonValidatorFunctions.isValidPrefixAndOrSuffix( - 'a/b', - '/', - () => false, - () => true, - ), - ).toBe(false); - expect( - CommonValidatorFunctions.isValidPrefixAndOrSuffix( - 'a/b', - '/', - () => true, - () => false, - ), - ).toBe(false); - }); - }); - - it.each([ - [null, true], - [undefined, false], - [1, true], - ['a', true], - [() => 'a', false], - [Symbol('a'), false], - [[], true], - [[1], true], - [[undefined], false], - [{}, true], - [{ a: 1 }, true], - [{ a: undefined }, false], - ] as [any, boolean][])(`isJsonSafe %p ? %p`, (value, result) => { - expect(CommonValidatorFunctions.isJsonSafe(value)).toBe(result); - }); - - it.each([ - [null, false], - [7, false], - ['', false], - ['a', true], - ['a-b', true], - ['-a-b', false], - ['a-b-', false], - ['a--b', false], - ['a_b', false], - ['adam.bertil.caesar', true], - ['adam.ber-til.caesar', true], - ['adam.-bertil.caesar', false], - ['adam.bertil-.caesar', false], - ['adam/bertil.caesar', false], - [`a.${'b'.repeat(63)}.c`, true], - [`a.${'b'.repeat(64)}.c`, false], - [ - `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(61)}`, - true, - ], - [ - `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(62)}`, - false, - ], - ])(`isValidDnsSubdomain %p ? %p`, (value, result) => { - expect(CommonValidatorFunctions.isValidDnsSubdomain(value)).toBe(result); - }); - - it.each([ - [null, false], - [7, false], - ['', false], - ['a', true], - ['a-b', true], - ['-a-b', false], - ['a-b-', false], - ['a--b', false], - ['a_b', false], - [`${'a'.repeat(63)}`, true], - [`${'a'.repeat(64)}`, false], - ])(`isValidDnsLabel %p ? %p`, (value, result) => { - expect(CommonValidatorFunctions.isValidDnsLabel(value)).toBe(result); - }); - - it.each([ - ['', ''], - ['a', 'a'], - ['a-b', 'ab'], - ['-a-b', 'ab'], - ['a_b', 'ab'], - [`${'a'.repeat(6000)}`, `${'a'.repeat(6000)}`], - ['_:;>!"#€', ''], - ])(`normalizeToLowercaseAlphanum %p ? %p`, (value, result) => { - expect(CommonValidatorFunctions.normalizeToLowercaseAlphanum(value)).toBe( - result, - ); - }); -}); diff --git a/plugins/catalog-backend/src/validation/CommonValidatorFunctions.ts b/plugins/catalog-backend/src/validation/CommonValidatorFunctions.ts deleted file mode 100644 index 96a91aca06..0000000000 --- a/plugins/catalog-backend/src/validation/CommonValidatorFunctions.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import lodash from 'lodash'; - -/** - * Contains various helper validation and normalization functions that can be - * composed to form a Validator. - */ -export class CommonValidatorFunctions { - /** - * Checks that the value is on the form or , and validates - * those parts separately. - * - * @param value The value to check - * @param separator The separator between parts - * @param isValidPrefix Checks that the part before the separator is valid, if present - * @param isValidSuffix Checks that the part after the separator (or the entire value if there is no separator) is valid - */ - static isValidPrefixAndOrSuffix( - value: any, - separator: string, - isValidPrefix: (value: string) => boolean, - isValidSuffix: (value: string) => boolean, - ): boolean { - if (typeof value !== 'string') { - return false; - } - - const parts = value.split(separator); - if (parts.length === 1) { - return isValidSuffix(parts[0]); - } else if (parts.length === 2) { - return isValidPrefix(parts[0]) && isValidSuffix(parts[1]); - } - - return false; - } - - /** - * Checks that the value can be safely transferred as JSON. - * - * @param value The value to check - */ - static isJsonSafe(value: any): boolean { - try { - return lodash.isEqual(value, JSON.parse(JSON.stringify(value))); - } catch { - return false; - } - } - - /** - * Checks that the value is a valid DNS subdomain name. - * - * @param value The value to check - * @see https://tools.ietf.org/html/rfc1123 - */ - static isValidDnsSubdomain(value: any): boolean { - return ( - typeof value === 'string' && - value.length >= 1 && - value.length <= 253 && - value.split('.').every(CommonValidatorFunctions.isValidDnsLabel) - ); - } - - /** - * Checks that the value is a valid DNS label. - * - * @param value The value to check - * @see https://tools.ietf.org/html/rfc1123 - */ - static isValidDnsLabel(value: any): boolean { - return ( - typeof value === 'string' && - value.length >= 1 && - value.length <= 63 && - /^[a-z0-9]+(\-[a-z0-9]+)*$/.test(value) - ); - } - - /** - * Normalizes by keeping only a-z, A-Z, and 0-9; and converts to lowercase. - * - * @param value The value to normalize - */ - static normalizeToLowercaseAlphanum(value: string): string { - return value - .split('') - .filter(x => /[a-zA-Z0-9]/.test(x)) - .join('') - .toLowerCase(); - } -} diff --git a/plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.test.ts b/plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.test.ts deleted file mode 100644 index d0673085b4..0000000000 --- a/plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions'; - -describe('KubernetesValidatorFunctions', () => { - it.each([ - [7, false], - [null, false], - ['', false], - ['a', true], - ['AZ09', true], - ['a'.repeat(63), true], - ['a'.repeat(64), false], - ['a-b', false], - ['a_b', false], - ['a.b', false], - ['a/a', true], - ['a/aAb5C', true], - ['a-b.c/v1', true], - ['a--b.c/v1', false], - [ - `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( - 61, - )}/v1`, - true, - ], - [ - `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( - 62, - )}/v1`, - false, - ], - [`a/${'a'.repeat(63)}`, true], - [`a/${'a'.repeat(64)}`, false], - ])(`isValidApiVersion %p ? %p`, (value, matches) => { - expect(KubernetesValidatorFunctions.isValidApiVersion(value)).toBe(matches); - }); - - it.each([ - [7, false], - [null, false], - ['', false], - ['a', true], - ['AZ09', true], - ['9AZ', false], - ['a'.repeat(63), true], - ['a'.repeat(64), false], - ['a-b', false], - ])(`isValidKind %p ? %p`, (value, matches) => { - expect(KubernetesValidatorFunctions.isValidKind(value)).toBe(matches); - }); - - it.each([ - [7, false], - [null, false], - ['', false], - ['a', true], - ['AZ09', true], - ['a'.repeat(63), true], - ['a'.repeat(64), false], - ['a/b', false], - ['a-b', true], - ['-a-b', false], - ['a-b-', false], - ['a--b', false], - ['a_b', true], - ['a.b', true], - ])(`isValidObjectName %p ? %p`, (value, matches) => { - expect(KubernetesValidatorFunctions.isValidObjectName(value)).toBe(matches); - }); - - it.each([ - [7, false], - [null, false], - ['', false], - ['a', true], - ['AZ09', false], - ['a'.repeat(63), true], - ['a'.repeat(64), false], - ['a/b', false], - ['a-b', true], - ['-a-b', false], - ['a-b-', false], - ['a--b', false], - ['a_b', false], - ['a.b', false], - ])(`isValidNamespace %p ? %p`, (value, matches) => { - expect(KubernetesValidatorFunctions.isValidNamespace(value)).toBe(matches); - }); - - it.each([ - [7, false], - [null, false], - ['', false], - ['a', true], - ['AZ09', true], - ['a'.repeat(63), true], - ['a'.repeat(64), false], - ['a/b', true], - ['a-b', true], - ['-a-b', false], - ['a-b-', false], - ['a--b', false], - ['a_b', true], - ['a.b', true], - ['a/a', true], - ['a-b.c/a', true], - ['a--b.c/a', false], - [ - `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( - 61, - )}/a`, - true, - ], - [ - `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( - 62, - )}/a`, - false, - ], - [`a/${'a'.repeat(63)}`, true], - [`a/${'a'.repeat(64)}`, false], - ])(`isValidLabelKey %p ? %p`, (value, matches) => { - expect(KubernetesValidatorFunctions.isValidLabelKey(value)).toBe(matches); - }); - - it.each([ - [7, false], - [null, false], - ['', true], - ['a', true], - ['AZ09', true], - ['a'.repeat(63), true], - ['a'.repeat(64), false], - ['a/b', false], - ['a-b', true], - ['-a-b', false], - ['a-b-', false], - ['a--b', false], - ['a_b', true], - ['a.b', true], - ])(`isValidLabelValue %p ? %p`, (value, matches) => { - expect(KubernetesValidatorFunctions.isValidLabelValue(value)).toBe(matches); - }); - - it.each([ - [7, false], - [null, false], - ['', false], - ['a', true], - ['AZ09', true], - ['a'.repeat(63), true], - ['a'.repeat(64), false], - ['a/b', true], - ['a-b', true], - ['-a-b', false], - ['a-b-', false], - ['a--b', false], - ['a_b', true], - ['a.b', true], - ['a/a', true], - ['a-b.c/a', true], - ['a--b.c/a', false], - [ - `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( - 61, - )}/a`, - true, - ], - [ - `${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat(63)}.${'a'.repeat( - 62, - )}/a`, - false, - ], - [`a/${'a'.repeat(63)}`, true], - [`a/${'a'.repeat(64)}`, false], - ])(`isValidAnnotationKey %p ? %p`, (value, matches) => { - expect(KubernetesValidatorFunctions.isValidAnnotationKey(value)).toBe( - matches, - ); - }); - - it.each([ - [7, false], - [null, false], - ['', true], - ['a', true], - ['/'.repeat(6000), true], - ])(`isValidAnnotationValue %p ? %p`, (value, matches) => { - expect(KubernetesValidatorFunctions.isValidAnnotationValue(value)).toBe( - matches, - ); - }); -}); diff --git a/plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.ts b/plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.ts deleted file mode 100644 index fa938f5fcb..0000000000 --- a/plugins/catalog-backend/src/validation/KubernetesValidatorFunctions.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CommonValidatorFunctions } from './CommonValidatorFunctions'; - -/** - * Contains validation functions that match the Kubernetes spec, usable to - * build a catalog that is compatible with those rule sets. - * - * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ - * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set - * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set - */ -export class KubernetesValidatorFunctions { - static isValidApiVersion(value: any): boolean { - return CommonValidatorFunctions.isValidPrefixAndOrSuffix( - value, - '/', - CommonValidatorFunctions.isValidDnsSubdomain, - n => n.length >= 1 && n.length <= 63 && /^[a-z0-9A-Z]+$/.test(n), - ); - } - - static isValidKind(value: any): boolean { - return ( - typeof value === 'string' && - value.length >= 1 && - value.length <= 63 && - /^[a-zA-Z][a-z0-9A-Z]*$/.test(value) - ); - } - - static isValidObjectName(value: any): boolean { - return ( - typeof value === 'string' && - value.length >= 1 && - value.length <= 63 && - /^[a-z0-9A-Z]+([-_.][a-z0-9A-Z]+)*$/.test(value) - ); - } - - static isValidNamespace(value: any): boolean { - return CommonValidatorFunctions.isValidDnsLabel(value); - } - - static isValidLabelKey(value: any): boolean { - return CommonValidatorFunctions.isValidPrefixAndOrSuffix( - value, - '/', - CommonValidatorFunctions.isValidDnsSubdomain, - KubernetesValidatorFunctions.isValidObjectName, - ); - } - - static isValidLabelValue(value: any): boolean { - return ( - value === '' || KubernetesValidatorFunctions.isValidObjectName(value) - ); - } - - static isValidAnnotationKey(value: any): boolean { - return CommonValidatorFunctions.isValidPrefixAndOrSuffix( - value, - '/', - CommonValidatorFunctions.isValidDnsSubdomain, - KubernetesValidatorFunctions.isValidObjectName, - ); - } - - static isValidAnnotationValue(value: any): boolean { - return typeof value === 'string'; - } -} diff --git a/plugins/catalog-backend/src/validation/index.ts b/plugins/catalog-backend/src/validation/index.ts deleted file mode 100644 index be607e43ec..0000000000 --- a/plugins/catalog-backend/src/validation/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -export * from './CommonValidatorFunctions'; -export * from './KubernetesValidatorFunctions'; -export * from './makeValidator'; -export * from './types'; diff --git a/plugins/catalog-backend/src/validation/makeValidator.ts b/plugins/catalog-backend/src/validation/makeValidator.ts deleted file mode 100644 index 7ca01365e0..0000000000 --- a/plugins/catalog-backend/src/validation/makeValidator.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CommonValidatorFunctions } from './CommonValidatorFunctions'; -import { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions'; -import { Validators } from './types'; - -const defaultValidators: Validators = { - isValidApiVersion: KubernetesValidatorFunctions.isValidApiVersion, - isValidKind: KubernetesValidatorFunctions.isValidKind, - isValidEntityName: KubernetesValidatorFunctions.isValidObjectName, - isValidNamespace: KubernetesValidatorFunctions.isValidNamespace, - normalizeEntityName: CommonValidatorFunctions.normalizeToLowercaseAlphanum, - isValidLabelKey: KubernetesValidatorFunctions.isValidLabelKey, - isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue, - isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey, - isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue, -}; - -export function makeValidator(overrides: Partial = {}): Validators { - return { - ...defaultValidators, - ...overrides, - }; -} diff --git a/plugins/catalog-backend/src/validation/types.ts b/plugins/catalog-backend/src/validation/types.ts deleted file mode 100644 index 81209bfb75..0000000000 --- a/plugins/catalog-backend/src/validation/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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. - */ - -export type Validators = { - isValidApiVersion(value: any): boolean; - isValidKind(value: any): boolean; - isValidEntityName(value: any): boolean; - isValidNamespace(value: any): boolean; - normalizeEntityName(value: string): string; - isValidLabelKey(value: any): boolean; - isValidLabelValue(value: any): boolean; - isValidAnnotationKey(value: any): boolean; - isValidAnnotationValue(value: any): boolean; -}; From 0ce506ac43684de588c5dc823d4f40ebc1c8d097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 16:36:52 +0200 Subject: [PATCH 17/97] Less watching, less foreign root fields --- packages/backend/package.json | 5 +---- packages/catalog-model/src/EntityPolicies.ts | 4 ++-- ...cy.test.ts => NoForeignRootFieldsEntityPolicy.test.ts} | 8 ++++---- ...EntityPolicy.ts => NoForeignRootFieldsEntityPolicy.ts} | 2 +- packages/catalog-model/src/entity/policies/index.ts | 2 +- 5 files changed, 9 insertions(+), 12 deletions(-) rename packages/catalog-model/src/entity/policies/{ForeignRootFieldsEntityPolicy.test.ts => NoForeignRootFieldsEntityPolicy.test.ts} (85%) rename packages/catalog-model/src/entity/policies/{ForeignRootFieldsEntityPolicy.ts => NoForeignRootFieldsEntityPolicy.ts} (94%) diff --git a/packages/backend/package.json b/packages/backend/package.json index 51fe04e51d..e4b77c1e41 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -45,9 +45,6 @@ "typescript": "^3.9.2" }, "nodemonConfig": { - "watch": [ - "./dist", - "node_modules/@backstage*" - ] + "watch": "./dist" } } diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index cedf0df13d..abc953ab6f 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -17,7 +17,7 @@ import { Entity, FieldFormatEntityPolicy, - ForeignRootFieldsEntityPolicy, + NoForeignRootFieldsEntityPolicy, ReservedFieldsEntityPolicy, SchemaValidEntityPolicy, } from './entity'; @@ -62,7 +62,7 @@ export class EntityPolicies implements EntityPolicy { return EntityPolicies.allOf([ EntityPolicies.allOf([ new SchemaValidEntityPolicy(), - new ForeignRootFieldsEntityPolicy(), + new NoForeignRootFieldsEntityPolicy(), new FieldFormatEntityPolicy(), new ReservedFieldsEntityPolicy(), ]), diff --git a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts similarity index 85% rename from packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts rename to packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts index 98299259f8..190024bb29 100644 --- a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts @@ -15,11 +15,11 @@ */ import yaml from 'yaml'; -import { ForeignRootFieldsEntityPolicy } from './ForeignRootFieldsEntityPolicy'; +import { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy'; -describe('ForeignRootFieldsEntityPolicy', () => { +describe('NoForeignRootFieldsEntityPolicy', () => { let data: any; - let policy: ForeignRootFieldsEntityPolicy; + let policy: NoForeignRootFieldsEntityPolicy; beforeEach(() => { data = yaml.parse(` @@ -38,7 +38,7 @@ describe('ForeignRootFieldsEntityPolicy', () => { spec: custom: stuff `); - policy = new ForeignRootFieldsEntityPolicy(); + policy = new NoForeignRootFieldsEntityPolicy(); }); it('works for the happy path', async () => { diff --git a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts similarity index 94% rename from packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts rename to packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts index a4733e9a42..3c0a2f5d61 100644 --- a/packages/catalog-model/src/entity/policies/ForeignRootFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts @@ -22,7 +22,7 @@ const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec']; /** * Ensures that there are no foreign root fields in the entity. */ -export class ForeignRootFieldsEntityPolicy implements EntityPolicy { +export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { private readonly knownFields: string[]; constructor(knownFields: string[] = defaultKnownFields) { diff --git a/packages/catalog-model/src/entity/policies/index.ts b/packages/catalog-model/src/entity/policies/index.ts index f43aa68049..d64053f7cb 100644 --- a/packages/catalog-model/src/entity/policies/index.ts +++ b/packages/catalog-model/src/entity/policies/index.ts @@ -15,6 +15,6 @@ */ export { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy'; -export { ForeignRootFieldsEntityPolicy } from './ForeignRootFieldsEntityPolicy'; +export { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy'; export { ReservedFieldsEntityPolicy } from './ReservedFieldsEntityPolicy'; export { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy'; From 709e3381d8d6a1418a436200e9fd0445aebafd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 20:50:46 +0200 Subject: [PATCH 18/97] Address comments --- packages/catalog-model/src/EntityPolicies.ts | 12 ++--- .../policies/FieldFormatEntityPolicy.test.ts | 30 +++++------ .../policies/FieldFormatEntityPolicy.ts | 2 +- .../NoForeignRootFieldsEntityPolicy.test.ts | 4 +- .../NoForeignRootFieldsEntityPolicy.ts | 2 +- .../ReservedFieldsEntityPolicy.test.ts | 10 ++-- .../policies/ReservedFieldsEntityPolicy.ts | 2 +- .../policies/SchemaValidEntityPolicy.test.ts | 52 +++++++++---------- .../policies/SchemaValidEntityPolicy.ts | 2 +- .../src/kinds/ComponentV1beta1.ts | 2 +- packages/catalog-model/src/setupTests.ts | 15 ------ packages/catalog-model/src/types.ts | 2 +- .../src/database/DatabaseManager.test.ts | 16 +++--- .../src/database/DatabaseManager.ts | 2 +- .../src/ingestion/IngestionModels.ts | 2 +- 15 files changed, 73 insertions(+), 82 deletions(-) delete mode 100644 packages/catalog-model/src/setupTests.ts diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index abc953ab6f..fa25334d5f 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -29,10 +29,10 @@ import { EntityPolicy } from './types'; class AllEntityPolicies implements EntityPolicy { constructor(private readonly policies: EntityPolicy[]) {} - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { let result = entity; for (const policy of this.policies) { - result = await policy.apply(entity); + result = await policy.enforce(entity); } return result; } @@ -43,10 +43,10 @@ class AllEntityPolicies implements EntityPolicy { class AnyEntityPolicy implements EntityPolicy { constructor(private readonly policies: EntityPolicy[]) {} - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { for (const policy of this.policies) { try { - return await policy.apply(entity); + return await policy.enforce(entity); } catch { continue; } @@ -82,7 +82,7 @@ export class EntityPolicies implements EntityPolicy { this.policy = policy; } - apply(entity: Entity): Promise { - return this.policy.apply(entity); + enforce(entity: Entity): Promise { + return this.policy.enforce(entity); } } diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index d81b1155be..14b44108e5 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -42,64 +42,64 @@ describe('FieldFormatEntityPolicy', () => { }); it('works for the happy path', async () => { - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad apiVersion', async () => { data.apiVersion = 7; - await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/); data.apiVersion = 'a#b'; - await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/); }); it('rejects bad kind', async () => { data.kind = 7; - await expect(policy.apply(data)).rejects.toThrow(/kind/); + await expect(policy.enforce(data)).rejects.toThrow(/kind/); data.kind = 'a#b'; - await expect(policy.apply(data)).rejects.toThrow(/kind/); + await expect(policy.enforce(data)).rejects.toThrow(/kind/); }); it('handles missing metadata gracefully', async () => { delete data.medatata; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('handles missing spec gracefully', async () => { delete data.spec; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad name', async () => { data.metadata.name = 7; - await expect(policy.apply(data)).rejects.toThrow(/name.*7/); + await expect(policy.enforce(data)).rejects.toThrow(/name.*7/); data.metadata.name = 'a'.repeat(1000); - await expect(policy.apply(data)).rejects.toThrow(/name.*aaaa/); + await expect(policy.enforce(data)).rejects.toThrow(/name.*aaaa/); }); it('rejects bad namespace', async () => { data.metadata.namespace = 7; - await expect(policy.apply(data)).rejects.toThrow(/namespace.*7/); + await expect(policy.enforce(data)).rejects.toThrow(/namespace.*7/); data.metadata.namespace = 'a'.repeat(1000); - await expect(policy.apply(data)).rejects.toThrow(/namespace.*aaaa/); + await expect(policy.enforce(data)).rejects.toThrow(/namespace.*aaaa/); }); it('rejects bad label key', async () => { data.metadata.labels['a#b'] = 'value'; - await expect(policy.apply(data)).rejects.toThrow(/label.*a#b/i); + await expect(policy.enforce(data)).rejects.toThrow(/label.*a#b/i); }); it('rejects bad label value', async () => { data.metadata.labels.a = 'a#b'; - await expect(policy.apply(data)).rejects.toThrow(/label.*a#b/i); + await expect(policy.enforce(data)).rejects.toThrow(/label.*a#b/i); }); it('rejects bad annotation key', async () => { data.metadata.annotations['a#b'] = 'value'; - await expect(policy.apply(data)).rejects.toThrow(/annotation.*a#b/i); + await expect(policy.enforce(data)).rejects.toThrow(/annotation.*a#b/i); }); it('rejects bad annotation value', async () => { data.metadata.annotations.a = 7; - await expect(policy.apply(data)).rejects.toThrow(/annotation.*7/i); + await expect(policy.enforce(data)).rejects.toThrow(/annotation.*7/i); }); }); diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 1f94354f3f..697e45b371 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -32,7 +32,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { this.validators = validators; } - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { function require( field: string, value: any, diff --git a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts index 190024bb29..50496682e8 100644 --- a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts @@ -42,11 +42,11 @@ describe('NoForeignRootFieldsEntityPolicy', () => { }); it('works for the happy path', async () => { - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects unknown root fields', async () => { data.spec2 = {}; - await expect(policy.apply(data)).rejects.toThrow(/spec2/i); + await expect(policy.enforce(data)).rejects.toThrow(/spec2/i); }); }); diff --git a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts index 3c0a2f5d61..9d1851bc02 100644 --- a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts @@ -29,7 +29,7 @@ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { this.knownFields = knownFields; } - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { for (const field of Object.keys(entity)) { if (!this.knownFields.includes(field)) { throw new Error(`Unknown field ${field}`); diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts index 348eabbdac..8db33955a7 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts @@ -42,21 +42,23 @@ describe('ReservedFieldsEntityPolicy', () => { }); it('works for the happy path', async () => { - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects reserved keys in the spec root', async () => { data.spec.apiVersion = 'a/b'; - await expect(policy.apply(data)).rejects.toThrow(/spec.*apiVersion/i); + await expect(policy.enforce(data)).rejects.toThrow(/spec.*apiVersion/i); }); it('rejects reserved keys in labels', async () => { data.metadata.labels.apiVersion = 'a'; - await expect(policy.apply(data)).rejects.toThrow(/label.*apiVersion/i); + await expect(policy.enforce(data)).rejects.toThrow(/label.*apiVersion/i); }); it('rejects reserved keys in annotations', async () => { data.metadata.annotations.apiVersion = 'a'; - await expect(policy.apply(data)).rejects.toThrow(/annotation.*apiVersion/i); + await expect(policy.enforce(data)).rejects.toThrow( + /annotation.*apiVersion/i, + ); }); }); diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts index be2f732ca4..d97469eecc 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts @@ -43,7 +43,7 @@ export class ReservedFieldsEntityPolicy implements EntityPolicy { ]; } - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { for (const field of this.reservedFields) { if (entity.spec?.hasOwnProperty(field)) { throw new Error( diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index b9d1165a60..d24aee9fe6 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -43,7 +43,7 @@ describe('SchemaValidEntityPolicy', () => { }); it('works for the happy path', async () => { - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); // @@ -51,113 +51,113 @@ describe('SchemaValidEntityPolicy', () => { // it('rejects wrong root type', async () => { - await expect(policy.apply((7 as unknown) as Entity)).rejects.toThrow( + await expect(policy.enforce((7 as unknown) as Entity)).rejects.toThrow( /object/, ); }); it('rejects missing apiVersion', async () => { delete data.apiVersion; - await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/); }); it('rejects bad apiVersion type', async () => { data.apiVersion = 7; - await expect(policy.apply(data)).rejects.toThrow(/apiVersion/); + await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/); }); it('rejects missing kind', async () => { delete data.kind; - await expect(policy.apply(data)).rejects.toThrow(/kind/); + await expect(policy.enforce(data)).rejects.toThrow(/kind/); }); it('rejects bad kind type', async () => { data.kind = 7; - await expect(policy.apply(data)).rejects.toThrow(/kind/); + await expect(policy.enforce(data)).rejects.toThrow(/kind/); }); // // metadata // - it('accepts missing metadata', async () => { - delete data.medatata; - await expect(policy.apply(data)).resolves.toBe(data); + it('rejects missing metadata', async () => { + delete data.metadata; + await expect(policy.enforce(data)).rejects.toThrow(/metadata/); }); it('rejects bad metadata type', async () => { data.metadata = 7; - await expect(policy.apply(data)).rejects.toThrow(/metadata/); + await expect(policy.enforce(data)).rejects.toThrow(/metadata/); }); it('accepts missing uid', async () => { delete data.metadata.uid; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad uid type', async () => { data.metadata.uid = 7; - await expect(policy.apply(data)).rejects.toThrow(/uid/); + await expect(policy.enforce(data)).rejects.toThrow(/uid/); }); it('accepts missing etag', async () => { delete data.metadata.etag; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad etag type', async () => { data.metadata.etag = 7; - await expect(policy.apply(data)).rejects.toThrow(/etag/); + await expect(policy.enforce(data)).rejects.toThrow(/etag/); }); it('accepts missing generation', async () => { delete data.metadata.generation; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad generation type', async () => { data.metadata.generation = 'a'; - await expect(policy.apply(data)).rejects.toThrow(/generation/); + await expect(policy.enforce(data)).rejects.toThrow(/generation/); }); it('accepts missing name', async () => { delete data.metadata.name; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad name type', async () => { data.metadata.name = 7; - await expect(policy.apply(data)).rejects.toThrow(/name/); + await expect(policy.enforce(data)).rejects.toThrow(/name/); }); it('accepts missing namespace', async () => { delete data.metadata.namespace; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad namespace type', async () => { data.metadata.namespace = 7; - await expect(policy.apply(data)).rejects.toThrow(/namespace/); + await expect(policy.enforce(data)).rejects.toThrow(/namespace/); }); it('accepts missing labels', async () => { delete data.metadata.labels; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad labels type', async () => { data.metadata.labels = 7; - await expect(policy.apply(data)).rejects.toThrow(/labels/); + await expect(policy.enforce(data)).rejects.toThrow(/labels/); }); it('accepts missing annotations', async () => { delete data.metadata.annotations; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad annotations type', async () => { data.metadata.annotations = 7; - await expect(policy.apply(data)).rejects.toThrow(/annotations/); + await expect(policy.enforce(data)).rejects.toThrow(/annotations/); }); // @@ -166,11 +166,11 @@ describe('SchemaValidEntityPolicy', () => { it('accepts missing spec', async () => { delete data.spec; - await expect(policy.apply(data)).resolves.toBe(data); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects non-object spec', async () => { data.spec = 7; - await expect(policy.apply(data)).rejects.toThrow(/spec/); + await expect(policy.enforce(data)).rejects.toThrow(/spec/); }); }); diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index 7c0f5c20b6..3ae20a5094 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -70,7 +70,7 @@ export class SchemaValidEntityPolicy implements EntityPolicy { this.schema = schema; } - async apply(entity: Entity): Promise { + async enforce(entity: Entity): Promise { try { return await this.schema.validate(entity, { strict: true }); } catch (e) { diff --git a/packages/catalog-model/src/kinds/ComponentV1beta1.ts b/packages/catalog-model/src/kinds/ComponentV1beta1.ts index b0a3f627a5..b041bf7967 100644 --- a/packages/catalog-model/src/kinds/ComponentV1beta1.ts +++ b/packages/catalog-model/src/kinds/ComponentV1beta1.ts @@ -50,7 +50,7 @@ export class ComponentV1beta1Policy implements EntityPolicy { }); } - async apply(envelope: Entity): Promise { + async enforce(envelope: Entity): Promise { if ( envelope.apiVersion !== 'backstage.io/v1beta1' || envelope.kind !== 'Component' diff --git a/packages/catalog-model/src/setupTests.ts b/packages/catalog-model/src/setupTests.ts deleted file mode 100644 index f3b69cc361..0000000000 --- a/packages/catalog-model/src/setupTests.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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. - */ diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 1d581cf23f..29ca8bdfa3 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -28,5 +28,5 @@ export type EntityPolicy = { * @returns The incoming entity, or a mutated version of the same * @throws An error if the entity should be rejected */ - apply(entity: Entity): Promise; + enforce(entity: Entity): Promise; }; diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts index c8d0aec332..9a4297904a 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.test.ts @@ -32,14 +32,14 @@ describe('DatabaseManager', () => { readLocation: jest.fn(), }; const policy: EntityPolicy = { - apply: jest.fn(), + enforce: jest.fn(), }; await expect( DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), ).resolves.toBeUndefined(); expect(reader.readLocation).not.toHaveBeenCalled(); - expect(policy.apply).not.toHaveBeenCalled(); + expect(policy.enforce).not.toHaveBeenCalled(); }); it('can update a single location', async () => { @@ -70,7 +70,7 @@ describe('DatabaseManager', () => { ), }; const policy: EntityPolicy = { - apply: jest.fn(() => Promise.resolve(desc)), + enforce: jest.fn(() => Promise.resolve(desc)), }; await expect( @@ -118,7 +118,7 @@ describe('DatabaseManager', () => { ), }; const policy: EntityPolicy = { - apply: jest.fn(() => Promise.resolve(desc)), + enforce: jest.fn(() => Promise.resolve(desc)), }; await expect( @@ -170,7 +170,9 @@ describe('DatabaseManager', () => { ), }; const policy: EntityPolicy = { - apply: jest.fn(() => Promise.reject(new Error('parser error message'))), + enforce: jest.fn(() => + Promise.reject(new Error('parser error message')), + ), }; await expect( @@ -217,7 +219,9 @@ describe('DatabaseManager', () => { ), }; const policy: EntityPolicy = { - apply: jest.fn(() => Promise.reject(new Error('parser error message'))), + enforce: jest.fn(() => + Promise.reject(new Error('parser error message')), + ), }; await expect( diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index a3aded18ec..c6e7a2e684 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -86,7 +86,7 @@ export class DatabaseManager { } try { - const entity = await entityPolicy.apply(readerItem.data); + const entity = await entityPolicy.enforce(readerItem.data); await DatabaseManager.refreshSingleEntity( database, location.id, diff --git a/plugins/catalog-backend/src/ingestion/IngestionModels.ts b/plugins/catalog-backend/src/ingestion/IngestionModels.ts index 616f1b49ae..def6f1fa5c 100644 --- a/plugins/catalog-backend/src/ingestion/IngestionModels.ts +++ b/plugins/catalog-backend/src/ingestion/IngestionModels.ts @@ -60,7 +60,7 @@ export class IngestionModels implements IngestionModel { result.push(item); } else { try { - const output = await this.entityPolicy.apply(item.data); + const output = await this.entityPolicy.enforce(item.data); result.push({ type: 'data', data: output }); } catch (e) { result.push({ type: 'error', error: e }); From dad0b8390cb5c23a9baec4ec47e1546c800cbcdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 20:54:02 +0200 Subject: [PATCH 19/97] Update plugins/catalog-backend/src/ingestion/source/index.ts Co-authored-by: Ivan Shmidt --- plugins/catalog-backend/src/ingestion/source/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/ingestion/source/index.ts b/plugins/catalog-backend/src/ingestion/source/index.ts index 3ed1063878..db7aa2f0bd 100644 --- a/plugins/catalog-backend/src/ingestion/source/index.ts +++ b/plugins/catalog-backend/src/ingestion/source/index.ts @@ -17,4 +17,4 @@ export { LocationReaders } from './LocationReaders'; export { FileLocationReader } from './readers/FileLocationReader'; export { GitHubLocationReader } from './readers/GitHubLocationReader'; -export { LocationReader } from './readers/types'; +export type { LocationReader } from './readers/types'; From 06879f55425747397fe4ce194efefd436a702245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 21:01:43 +0200 Subject: [PATCH 20/97] Accidental premature test fix :) --- .../src/entity/policies/SchemaValidEntityPolicy.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index d24aee9fe6..0837b75759 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -80,9 +80,9 @@ describe('SchemaValidEntityPolicy', () => { // metadata // - it('rejects missing metadata', async () => { + it('accepts missing metadata', async () => { delete data.metadata; - await expect(policy.enforce(data)).rejects.toThrow(/metadata/); + await expect(policy.enforce(data)).resolves.toBe(data); }); it('rejects bad metadata type', async () => { From 60e1ad28aac8a06c9d5be8ce8c2f1d7a580fb3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 17:00:57 +0200 Subject: [PATCH 21/97] Make metadata and name mandatory --- packages/catalog-model/src/entity/Entity.ts | 8 +- .../policies/FieldFormatEntityPolicy.ts | 13 +-- .../policies/ReservedFieldsEntityPolicy.ts | 4 +- .../policies/SchemaValidEntityPolicy.test.ts | 8 +- .../policies/SchemaValidEntityPolicy.ts | 4 +- .../src/catalog/StaticEntitiesCatalog.ts | 6 +- .../src/database/Database.test.ts | 92 +++++++++++-------- .../catalog-backend/src/database/Database.ts | 40 +++----- .../src/database/DatabaseManager.ts | 4 +- .../migrations/20200511113813_init.ts | 2 +- .../src/database/search.test.ts | 5 +- .../catalog-backend/src/database/search.ts | 6 +- plugins/catalog-backend/src/database/types.ts | 2 +- .../src/service/router.test.ts | 8 +- 14 files changed, 102 insertions(+), 100 deletions(-) diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 6f57626749..80c5765ee1 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -32,9 +32,9 @@ export type Entity = { kind: string; /** - * Optional metadata related to the entity. + * Metadata related to the entity. */ - metadata?: EntityMeta; + metadata: EntityMeta; /** * The specification data describing the entity itself. @@ -86,9 +86,9 @@ export type EntityMeta = { * The name of the entity. * * Must be uniqe within the catalog at any given point in time, for any - * given namespace, for any given kind. + * given namespace + kind pair. */ - name?: string; + name: string; /** * The namespace that the entity belongs to. diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 697e45b371..4ccd8ec711 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -65,23 +65,20 @@ export class FieldFormatEntityPolicy implements EntityPolicy { require('apiVersion', entity.apiVersion, this.validators.isValidApiVersion); require('kind', entity.kind, this.validators.isValidKind); - optional( - 'metadata.name', - entity.metadata?.name, - this.validators.isValidEntityName, - ); + require('metadata.name', entity.metadata.name, this.validators + .isValidEntityName); optional( 'metadata.namespace', - entity.metadata?.namespace, + entity.metadata.namespace, this.validators.isValidNamespace, ); - for (const [k, v] of Object.entries(entity.metadata?.labels ?? [])) { + for (const [k, v] of Object.entries(entity.metadata.labels ?? [])) { require(`labels.${k}`, k, this.validators.isValidLabelKey); require(`labels.${k}`, v, this.validators.isValidLabelValue); } - for (const [k, v] of Object.entries(entity.metadata?.annotations ?? [])) { + for (const [k, v] of Object.entries(entity.metadata.annotations ?? [])) { require(`annotations.${k}`, k, this.validators.isValidAnnotationKey); require(`annotations.${k}`, v, this.validators.isValidAnnotationValue); } diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts index d97469eecc..57029ffa24 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts @@ -50,12 +50,12 @@ export class ReservedFieldsEntityPolicy implements EntityPolicy { `The spec may not contain the field ${field}, because it has reserved meaning`, ); } - if (entity.metadata?.labels?.hasOwnProperty(field)) { + if (entity.metadata.labels?.hasOwnProperty(field)) { throw new Error( `A label may not have the field ${field}, because it has reserved meaning`, ); } - if (entity.metadata?.annotations?.hasOwnProperty(field)) { + if (entity.metadata.annotations?.hasOwnProperty(field)) { throw new Error( `An annotation may not have the field ${field}, because it has reserved meaning`, ); diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index 0837b75759..c53fbf3e46 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -80,9 +80,9 @@ describe('SchemaValidEntityPolicy', () => { // metadata // - it('accepts missing metadata', async () => { + it('rejects missing metadata', async () => { delete data.metadata; - await expect(policy.enforce(data)).resolves.toBe(data); + await expect(policy.enforce(data)).rejects.toThrow(/metadata/); }); it('rejects bad metadata type', async () => { @@ -120,9 +120,9 @@ describe('SchemaValidEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/generation/); }); - it('accepts missing name', async () => { + it('rejects missing name', async () => { delete data.metadata.name; - await expect(policy.enforce(data)).resolves.toBe(data); + await expect(policy.enforce(data)).rejects.toThrow(/name/); }); it('rejects bad name type', async () => { diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index 3ae20a5094..d367022ff1 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -47,12 +47,12 @@ const DEFAULT_ENTITY_SCHEMA = yup.object({ 'The generation must be an integer greater than zero', value => value === undefined || (value === (value | 0) && value > 0), ), - name: yup.string().notRequired(), + name: yup.string().required(), namespace: yup.string().notRequired(), labels: yup.object>().notRequired(), annotations: yup.object>().notRequired(), }) - .notRequired(), + .required(), spec: yup.object({}).notRequired(), }); diff --git a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts index 1de606d44d..64ac5f57d5 100644 --- a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts @@ -31,7 +31,7 @@ export class StaticEntitiesCatalog implements EntitiesCatalog { } async entityByUid(uid: string): Promise { - const item = this._entities.find(e => uid === e.metadata?.uid); + const item = this._entities.find(e => uid === e.metadata.uid); if (!item) { throw new NotFoundError('Entity cannot be found'); } @@ -46,8 +46,8 @@ export class StaticEntitiesCatalog implements EntitiesCatalog { const item = this._entities.find( e => kind === e.kind && - name === e.metadata?.name && - namespace === e.metadata?.namespace, + name === e.metadata.name && + namespace === e.metadata.namespace, ); if (!item) { throw new NotFoundError('Entity cannot be found'); diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index 7a38adf02f..bb1a66ac7c 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -128,7 +128,7 @@ describe('Database', () => { catalog.addEntity(tx, entityRequest), ); expect(added).toStrictEqual(entityResponse); - expect(added.entity.metadata!.generation).toBe(1); + expect(added.entity.metadata.generation).toBe(1); }); it('rejects adding the same-named entity twice', async () => { @@ -141,9 +141,9 @@ describe('Database', () => { it('accepts adding the same-named entity twice if on different namespaces', async () => { const catalog = new Database(database, getVoidLogger()); - entityRequest.entity.metadata!.namespace = 'namespace1'; + entityRequest.entity.metadata.namespace = 'namespace1'; await catalog.transaction(tx => catalog.addEntity(tx, entityRequest)); - entityRequest.entity.metadata!.namespace = 'namespace2'; + entityRequest.entity.metadata.namespace = 'namespace2'; await expect( catalog.transaction(tx => catalog.addEntity(tx, entityRequest)), ).resolves.toBeDefined(); @@ -161,17 +161,15 @@ describe('Database', () => { ); expect(updated.entity.apiVersion).toEqual(added.entity.apiVersion); expect(updated.entity.kind).toEqual(added.entity.kind); - expect(updated.entity.metadata!.etag).not.toEqual( - added.entity.metadata!.etag, + expect(updated.entity.metadata.etag).not.toEqual( + added.entity.metadata.etag, ); - expect(updated.entity.metadata!.generation).toEqual( - added.entity.metadata!.generation, + expect(updated.entity.metadata.generation).toEqual( + added.entity.metadata.generation, ); - expect(updated.entity.metadata!.name).toEqual( - added.entity.metadata!.name, - ); - expect(updated.entity.metadata!.namespace).toEqual( - added.entity.metadata!.namespace, + expect(updated.entity.metadata.name).toEqual(added.entity.metadata.name); + expect(updated.entity.metadata.namespace).toEqual( + added.entity.metadata.namespace, ); }); @@ -180,11 +178,11 @@ describe('Database', () => { const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); - added.entity.metadata!.name! = 'new!'; + added.entity.metadata.name! = 'new!'; const updated = await catalog.transaction(tx => catalog.updateEntity(tx, { entity: added.entity }), ); - expect(updated.entity.metadata!.name).toEqual('new!'); + expect(updated.entity.metadata.name).toEqual('new!'); }); it('can update fields if kind, name, and namespace match', async () => { @@ -193,8 +191,8 @@ describe('Database', () => { catalog.addEntity(tx, entityRequest), ); added.entity.apiVersion = 'something.new'; - delete added.entity.metadata!.uid; - delete added.entity.metadata!.generation; + delete added.entity.metadata.uid; + delete added.entity.metadata.generation; const updated = await catalog.transaction(tx => catalog.updateEntity(tx, { entity: added.entity }), ); @@ -207,9 +205,9 @@ describe('Database', () => { catalog.addEntity(tx, entityRequest), ); added.entity.apiVersion = 'something.new'; - delete added.entity.metadata!.uid; - delete added.entity.metadata!.generation; - added.entity.metadata!.namespace = 'something.wrong'; + delete added.entity.metadata.uid; + delete added.entity.metadata.generation; + added.entity.metadata.namespace = 'something.wrong'; await expect( catalog.transaction(tx => catalog.updateEntity(tx, { entity: added.entity }), @@ -222,7 +220,7 @@ describe('Database', () => { const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); - added.entity.metadata!.etag = 'garbage'; + added.entity.metadata.etag = 'garbage'; await expect( catalog.transaction(tx => catalog.updateEntity(tx, { entity: added.entity }), @@ -235,7 +233,7 @@ describe('Database', () => { const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); - added.entity.metadata!.generation! += 100; + added.entity.metadata.generation! += 100; await expect( catalog.transaction(tx => catalog.updateEntity(tx, { entity: added.entity }), @@ -247,10 +245,15 @@ describe('Database', () => { describe('entities', () => { it('can get all entities with empty filters list', async () => { const catalog = new Database(database, getVoidLogger()); - const e1: Entity = { apiVersion: 'a', kind: 'b' }; - const e2: Entity = { + const e1: Entity = { apiVersion: 'a', - kind: 'b', + kind: 'k1', + metadata: { name: 'n' }, + }; + const e2: Entity = { + apiVersion: 'c', + kind: 'k2', + metadata: { name: 'n' }, spec: { c: null }, }; await catalog.transaction(async tx => { @@ -263,8 +266,14 @@ describe('Database', () => { expect(result.length).toEqual(2); expect(result).toEqual( expect.arrayContaining([ - { locationId: undefined, entity: expect.objectContaining(e1) }, - { locationId: undefined, entity: expect.objectContaining(e2) }, + { + locationId: undefined, + entity: expect.objectContaining({ kind: 'k1' }), + }, + { + locationId: undefined, + entity: expect.objectContaining({ kind: 'k2' }), + }, ]), ); }); @@ -272,15 +281,17 @@ describe('Database', () => { it('can get all specific entities for matching filters (naive case)', async () => { const catalog = new Database(database, getVoidLogger()); const entities: Entity[] = [ - { apiVersion: 'a', kind: 'b' }, + { apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } }, { apiVersion: 'a', - kind: 'b', + kind: 'k2', + metadata: { name: 'n' }, spec: { c: 'some' }, }, { apiVersion: 'a', - kind: 'b', + kind: 'k3', + metadata: { name: 'n' }, spec: { c: null }, }, ]; @@ -294,27 +305,32 @@ describe('Database', () => { await expect( catalog.transaction(async tx => catalog.entities(tx, [ - { key: 'kind', values: ['b'] }, + { key: 'kind', values: ['k2'] }, { key: 'spec.c', values: ['some'] }, ]), ), ).resolves.toEqual([ - { locationId: undefined, entity: expect.objectContaining(entities[1]) }, + { + locationId: undefined, + entity: expect.objectContaining({ kind: 'k2' }), + }, ]); }); it('can get all specific entities for matching filters with nulls (both missing and literal null value)', async () => { const catalog = new Database(database, getVoidLogger()); const entities: Entity[] = [ - { apiVersion: 'a', kind: 'b' }, + { apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } }, { apiVersion: 'a', - kind: 'b', + kind: 'k2', + metadata: { name: 'n' }, spec: { c: 'some' }, }, { apiVersion: 'a', - kind: 'b', + kind: 'k3', + metadata: { name: 'n' }, spec: { c: null }, }, ]; @@ -327,7 +343,7 @@ describe('Database', () => { const rows = await catalog.transaction(async tx => catalog.entities(tx, [ - { key: 'kind', values: ['b'] }, + { key: 'apiVersion', values: ['a'] }, { key: 'spec.c', values: [null, 'some'] }, ]), ); @@ -337,15 +353,15 @@ describe('Database', () => { expect.arrayContaining([ { locationId: undefined, - entity: expect.objectContaining(entities[0]), + entity: expect.objectContaining({ kind: 'k1' }), }, { locationId: undefined, - entity: expect.objectContaining(entities[1]), + entity: expect.objectContaining({ kind: 'k2' }), }, { locationId: undefined, - entity: expect.objectContaining(entities[2]), + entity: expect.objectContaining({ kind: 'k3' }), }, ]), ); diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index 1f5cc7562f..26deb3362e 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -46,11 +46,7 @@ function getStrippedMetadata(metadata: EntityMeta): EntityMeta { return output; } -function serializeMetadata(metadata: EntityMeta | undefined): string | null { - if (!metadata) { - return null; - } - +function serializeMetadata(metadata: EntityMeta): string { return JSON.stringify(getStrippedMetadata(metadata)); } @@ -67,14 +63,14 @@ function toEntityRow( entity: Entity, ): DbEntitiesRow { return { - id: entity.metadata!.uid!, + id: entity.metadata.uid!, location_id: locationId || null, - etag: entity.metadata!.etag!, - generation: entity.metadata!.generation!, + etag: entity.metadata.etag!, + generation: entity.metadata.generation!, api_version: entity.apiVersion, kind: entity.kind, - name: entity.metadata!.name || null, - namespace: entity.metadata!.namespace || null, + name: entity.metadata.name || null, + namespace: entity.metadata.namespace || null, metadata: serializeMetadata(entity.metadata), spec: serializeSpec(entity.spec), }; @@ -85,17 +81,13 @@ function toEntityResponse(row: DbEntitiesRow): DbEntityResponse { apiVersion: row.api_version, kind: row.kind, metadata: { + ...(JSON.parse(row.metadata) as Entity['metadata']), uid: row.id, etag: row.etag, generation: Number(row.generation), // cast because of sqlite }, }; - if (row.metadata) { - const metadata = JSON.parse(row.metadata) as Entity['metadata']; - entity.metadata = { ...entity.metadata, ...metadata }; - } - if (row.spec) { const spec = JSON.parse(row.spec); entity.spec = spec; @@ -125,9 +117,7 @@ function generateUid(): string { } function generateEtag(): string { - return Buffer.from(uuidv4(), 'utf8') - .toString('base64') - .replace(/[^\w]/g, ''); + return Buffer.from(uuidv4(), 'utf8').toString('base64').replace(/[^\w]/g, ''); } /** @@ -178,11 +168,11 @@ export class Database { tx: Knex.Transaction, request: DbEntityRequest, ): Promise { - if (request.entity.metadata?.uid !== undefined) { + if (request.entity.metadata.uid !== undefined) { throw new InputError('May not specify uid for new entities'); - } else if (request.entity.metadata?.etag !== undefined) { + } else if (request.entity.metadata.etag !== undefined) { throw new InputError('May not specify etag for new entities'); - } else if (request.entity.metadata?.generation !== undefined) { + } else if (request.entity.metadata.generation !== undefined) { throw new InputError('May not specify generation for new entities'); } @@ -289,9 +279,9 @@ export class Database { if (oldRow.metadata) { const oldMetadata = JSON.parse(oldRow.metadata) as EntityMeta; if (oldMetadata.annotations) { - newEntity.metadata!.annotations = { + newEntity.metadata.annotations = { ...oldMetadata.annotations, - ...newEntity.metadata!.annotations, + ...newEntity.metadata.annotations, }; } } @@ -374,9 +364,7 @@ export class Database { target, }); - return (await tx('locations') - .where({ id }) - .select())![0]; + return (await tx('locations').where({ id }).select())![0]; }); } diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index c6e7a2e684..05b6eaef8d 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -96,14 +96,14 @@ export class DatabaseManager { await DatabaseManager.logUpdateSuccess( database, location.id, - entity.metadata!.name, + entity.metadata.name, ); } catch (error) { await DatabaseManager.logUpdateFailure( database, location.id, error, - readerItem.data.metadata?.name, + readerItem.data.metadata.name, ); } } diff --git a/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts b/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts index f029871a70..2ff06c4046 100644 --- a/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts +++ b/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts @@ -76,7 +76,7 @@ export async function up(knex: Knex): Promise { .comment('The metadata.namespace field of the entity'); table .string('metadata') - .nullable() + .notNullable() .comment('The entire metadata JSON blob of the entity'); table .string('spec') diff --git a/plugins/catalog-backend/src/database/search.test.ts b/plugins/catalog-backend/src/database/search.test.ts index 7ad06aee14..8f011fb250 100644 --- a/plugins/catalog-backend/src/database/search.test.ts +++ b/plugins/catalog-backend/src/database/search.test.ts @@ -102,14 +102,15 @@ describe('search', () => { const input: Entity = { apiVersion: 'a', kind: 'b', + metadata: { name: 'n' }, }; expect(buildEntitySearch('eid', input)).toEqual([ - { entity_id: 'eid', key: 'metadata.name', value: null }, + { entity_id: 'eid', key: 'metadata.name', value: 'n' }, { entity_id: 'eid', key: 'metadata.namespace', value: null }, { entity_id: 'eid', key: 'metadata.uid', value: null }, { entity_id: 'eid', key: 'apiVersion', value: 'a' }, { entity_id: 'eid', key: 'kind', value: 'b' }, - { entity_id: 'eid', key: 'name', value: null }, + { entity_id: 'eid', key: 'name', value: 'n' }, { entity_id: 'eid', key: 'namespace', value: null }, { entity_id: 'eid', key: 'uid', value: null }, ]); diff --git a/plugins/catalog-backend/src/database/search.ts b/plugins/catalog-backend/src/database/search.ts index fcacf1a9f2..87fc59185d 100644 --- a/plugins/catalog-backend/src/database/search.ts +++ b/plugins/catalog-backend/src/database/search.ts @@ -127,17 +127,17 @@ export function buildEntitySearch( { entity_id: entityId, key: 'metadata.name', - value: toValue(entity.metadata?.name), + value: toValue(entity.metadata.name), }, { entity_id: entityId, key: 'metadata.namespace', - value: toValue(entity.metadata?.namespace), + value: toValue(entity.metadata.namespace), }, { entity_id: entityId, key: 'metadata.uid', - value: toValue(entity.metadata?.uid), + value: toValue(entity.metadata.uid), }, ]; diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index ca1cbbdfd4..9b5a91d249 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -26,7 +26,7 @@ export type DbEntitiesRow = { namespace: string | null; etag: string; generation: number; - metadata: string | null; + metadata: string; spec: string | null; }; diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 36e8b55770..f5675e6516 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -37,7 +37,9 @@ class MockLocationsCatalog implements LocationsCatalog { describe('createRouter', () => { describe('entities', () => { it('happy path: lists entities', async () => { - const entities: Entity[] = [{ apiVersion: 'a', kind: 'b' }]; + const entities: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; const catalog = new MockEntitiesCatalog(); catalog.entities.mockResolvedValueOnce(entities); @@ -190,9 +192,7 @@ describe('createRouter', () => { }); const app = express().use(router); - const response = await request(app) - .post('/locations') - .send(location); + const response = await request(app).post('/locations').send(location); expect(response.status).toEqual(400); }); From e607ecfb3de9db268d2444abcdc71b26b27a2e2c Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Thu, 28 May 2020 21:25:11 +0200 Subject: [PATCH 22/97] remove tests. all routes for google auth working --- .../src/providers/GoogleAuthProvider.ts | 76 --- .../auth-backend/src/providers/OAuthHelper.ts | 70 --- .../src/providers/OAuthProvider.ts | 75 ++- .../src/providers/PassportStrategyHelper.ts | 2 +- .../src/providers/factories.test.ts | 51 -- .../auth-backend/src/providers/factories.ts | 3 +- .../src/providers/google/index.ts | 2 +- .../src/providers/google/provider.test.ts | 531 ------------------ .../src/providers/google/provider.ts | 209 +++---- .../auth-backend/src/providers/index.test.ts | 100 ---- plugins/auth-backend/src/providers/types.ts | 30 +- 11 files changed, 139 insertions(+), 1010 deletions(-) delete mode 100644 plugins/auth-backend/src/providers/GoogleAuthProvider.ts delete mode 100644 plugins/auth-backend/src/providers/OAuthHelper.ts delete mode 100644 plugins/auth-backend/src/providers/factories.test.ts delete mode 100644 plugins/auth-backend/src/providers/google/provider.test.ts delete mode 100644 plugins/auth-backend/src/providers/index.test.ts diff --git a/plugins/auth-backend/src/providers/GoogleAuthProvider.ts b/plugins/auth-backend/src/providers/GoogleAuthProvider.ts deleted file mode 100644 index 604a765906..0000000000 --- a/plugins/auth-backend/src/providers/GoogleAuthProvider.ts +++ /dev/null @@ -1,76 +0,0 @@ -import express from 'express'; -import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -import { - executeFrameHandlerStrategy, - executeRedirectStrategy, - executeRefreshTokenStrategy, -} from './PassportStrategyHelper'; -import { - OAuthProviderHandlers, - AuthInfoBase, - AuthInfoPrivate, - RedirectInfo, - AuthProviderConfig, -} from './types'; - -export class GoogleAuthProvider implements OAuthProviderHandlers { - private readonly provider: string; - private readonly providerConfig: AuthProviderConfig; - private readonly _strategy: GoogleStrategy; - - constructor(providerConfig: AuthProviderConfig) { - this.provider = providerConfig.provider; - this.providerConfig = providerConfig; - // TODO: throw error if env variables not set? - this._strategy = new GoogleStrategy( - { ...this.providerConfig.options }, - ( - accessToken: any, - refreshToken: any, - params: any, - profile: any, - done: any, - ) => { - done( - undefined, - { - profile, - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - { - refreshToken, - }, - ); - }, - ); - } - - async start(req: express.Request, options: any): Promise { - return await executeRedirectStrategy(req, this._strategy, options); - } - - async handler( - req: express.Request, - ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { - return await executeFrameHandlerStrategy(req, this._strategy); - } - - async refresh(refreshToken: string, scope: string): Promise { - return await executeRefreshTokenStrategy( - this._strategy, - refreshToken, - scope, - ); - } - - logout(): Promise { - throw new Error('Method not implemented.'); - } - - getProvider(): string { - return this.provider; - } -} diff --git a/plugins/auth-backend/src/providers/OAuthHelper.ts b/plugins/auth-backend/src/providers/OAuthHelper.ts deleted file mode 100644 index 7f930530a9..0000000000 --- a/plugins/auth-backend/src/providers/OAuthHelper.ts +++ /dev/null @@ -1,70 +0,0 @@ -import express, { CookieOptions } from 'express'; -import crypto from 'crypto'; - -export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; -export const TEN_MINUTES_MS = 600 * 1000; - -// TODO: move all of these methods to OAuthProvider - -export const verifyNonce = (req: express.Request, provider: string) => { - const cookieNonce = req.cookies[`${provider}-nonce`]; - const stateNonce = req.query.state; - - if (!cookieNonce || !stateNonce) { - throw new Error('Missing nonce'); - } - - if (cookieNonce !== stateNonce) { - throw new Error('Invalid nonce'); - } -}; - -export const setNonceCookie = (res: express.Response, provider: string) => { - const nonce = crypto.randomBytes(16).toString('base64'); - - const options: CookieOptions = { - maxAge: TEN_MINUTES_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}/handler`, - httpOnly: true, - }; - - res.cookie(`${provider}-nonce`, nonce, options); - - return nonce; -}; - -export const setRefreshTokenCookie = ( - res: express.Response, - provider: string, - refreshToken: string, -) => { - const options: CookieOptions = { - maxAge: THOUSAND_DAYS_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}`, - httpOnly: true, - }; - - res.cookie(`${provider}-refresh-token`, refreshToken, options); -}; - -export const removeRefreshTokenCookie = ( - res: express.Response, - provider: string, -) => { - const options: CookieOptions = { - maxAge: 0, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}`, - httpOnly: true, - }; - - res.cookie(`${provider}-refresh-token`, '', options); -}; diff --git a/plugins/auth-backend/src/providers/OAuthProvider.ts b/plugins/auth-backend/src/providers/OAuthProvider.ts index d294654453..d02f7da110 100644 --- a/plugins/auth-backend/src/providers/OAuthProvider.ts +++ b/plugins/auth-backend/src/providers/OAuthProvider.ts @@ -1,14 +1,12 @@ +import express, { CookieOptions } from 'express'; +import crypto from 'crypto'; import { AuthProviderRouteHandlers, OAuthProviderHandlers } from './types'; -import express from 'express'; import { InputError } from '@backstage/backend-common'; -import { - setNonceCookie, - verifyNonce, - setRefreshTokenCookie, - removeRefreshTokenCookie, -} from './OAuthHelper'; import { postMessageResponse, ensuresXRequestedWith } from './utils'; +export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; +export const TEN_MINUTES_MS = 600 * 1000; + export class OAuthProvider implements AuthProviderRouteHandlers { private readonly provider: string; private readonly providerHandlers: OAuthProviderHandlers; @@ -114,3 +112,66 @@ export class OAuthProvider implements AuthProviderRouteHandlers { } } } + +export const verifyNonce = (req: express.Request, provider: string) => { + const cookieNonce = req.cookies[`${provider}-nonce`]; + const stateNonce = req.query.state; + + if (!cookieNonce || !stateNonce) { + throw new Error('Missing nonce'); + } + + if (cookieNonce !== stateNonce) { + throw new Error('Invalid nonce'); + } +}; + +export const setNonceCookie = (res: express.Response, provider: string) => { + const nonce = crypto.randomBytes(16).toString('base64'); + + const options: CookieOptions = { + maxAge: TEN_MINUTES_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}/handler`, + httpOnly: true, + }; + + res.cookie(`${provider}-nonce`, nonce, options); + + return nonce; +}; + +export const setRefreshTokenCookie = ( + res: express.Response, + provider: string, + refreshToken: string, +) => { + const options: CookieOptions = { + maxAge: THOUSAND_DAYS_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, refreshToken, options); +}; + +export const removeRefreshTokenCookie = ( + res: express.Response, + provider: string, +) => { + const options: CookieOptions = { + maxAge: 0, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, '', options); +}; diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts index e33c183381..515a91f25c 100644 --- a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts @@ -85,7 +85,7 @@ export const executeRefreshTokenStrategy = async ( resolve({ accessToken, idToken: params.id_token, - expiresInSeconds: params.expires_in, + expiresInSeconds: 10, scope: params.scope, }); }, diff --git a/plugins/auth-backend/src/providers/factories.test.ts b/plugins/auth-backend/src/providers/factories.test.ts deleted file mode 100644 index cc31834889..0000000000 --- a/plugins/auth-backend/src/providers/factories.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// /* -// * Copyright 2020 Spotify AB -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ - -// import express from 'express'; -// import passport from 'passport'; -// import { OAuthProviderHandlers } from './types'; -// import { ProviderFactories } from './factories'; - -// class MyAuthProvider implements OAuthProviderHandlers { -// async start(_: express.Request, res: express.Response): Promise { -// res.send('start'); -// } -// async logout(_: express.Request, res: express.Response): Promise { -// res.send('logout'); -// } -// async handler(): Promise { -// throw new Error('Method not implemented.'); -// } -// async refresh(): Promise { -// throw new Error('Method not implemented.'); -// } -// } - -// describe('getProviderFactory', () => { -// it('makes a provider for MyAuthProvider', () => { -// jest -// .spyOn(ProviderFactories, 'getProviderFactory') -// .mockReturnValueOnce(MyAuthProvider); -// const provider = ProviderFactories.getProviderFactory('a'); -// expect(provider).toBeDefined(); -// }); - -// it('throws an error when provider implementation does not exist', () => { -// expect(() => { -// ProviderFactories.getProviderFactory('b'); -// }).toThrow('Provider Implementation missing for : b auth provider'); -// }); -// }); diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 76c3bd3dde..0a1e639082 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -15,8 +15,7 @@ */ import { AuthProviderFactories, AuthProviderFactory } from './types'; -// import { GoogleAuthProvider } from './google/provider'; -import { GoogleAuthProvider } from './GoogleAuthProvider'; +import { GoogleAuthProvider } from './google'; export class ProviderFactories { private static readonly providerFactories: AuthProviderFactories = { diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index d79c9e34e9..5a94d88ada 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -1 +1 @@ -// export { GoogleAuthProvider } from './provider'; +export { GoogleAuthProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts deleted file mode 100644 index 03f3da1eda..0000000000 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ /dev/null @@ -1,531 +0,0 @@ -// /* -// * Copyright 2020 Spotify AB -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ - -// import { -// GoogleAuthProvider, -// THOUSAND_DAYS_MS, -// TEN_MINUTES_MS, -// } from './provider'; -// import passport from 'passport'; -// import express from 'express'; -// import * as utils from './../utils'; -// import refresh from 'passport-oauth2-refresh'; - -// const googleAuthProviderConfig = { -// provider: 'google', -// options: { -// clientID: 'a', -// clientSecret: 'b', -// callbackURL: 'c', -// }, -// }; - -// const googleAuthProviderConfigInvalidOptions = { -// provider: 'google', -// options: {}, -// }; - -// describe('GoogleAuthProvider', () => { -// afterEach(() => { -// jest.clearAllMocks(); -// }); -// describe('create a new provider', () => { -// it('should succeed with valid config', () => { -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); -// expect(googleAuthProvider).toBeDefined(); -// expect(googleAuthProvider.start).toBeDefined(); -// expect(googleAuthProvider.logout).toBeDefined(); -// expect(googleAuthProvider.frameHandler).toBeDefined(); -// expect(googleAuthProvider.strategy).toBeDefined(); -// }); -// }); - -// describe('start authentication handler', () => { -// const mockResponse = ({ -// send: jest.fn().mockReturnThis(), -// status: jest.fn().mockReturnThis(), -// cookie: jest.fn().mockReturnThis(), -// } as unknown) as express.Response; - -// fit('should initiate authenticate request with provided scopes', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// query: { -// scope: 'a,b', -// }, -// } as unknown) as express.Request; - -// // const spyPassport = jest -// // .spyOn(passport, 'authenticate') -// // .mockImplementation(() => jest.fn()); -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// const googleAuthProviderStrategy = googleAuthProvider.strategy(); -// const spyAuthenticate = jest -// .spyOn(googleAuthProviderStrategy, 'authenticate') -// .mockImplementation(() => jest.fn()); - -// // const spyRedirect = jest -// // .spyOn(googleAuthProviderStrategy, 'redirect') -// // .mockImplementation(() => jest.fn()); - -// googleAuthProvider.start(mockRequest, mockResponse); -// expect(spyAuthenticate).toBeCalledTimes(1); -// expect(spyAuthenticate).toBeCalledWith(mockRequest, { -// scope: 'a,b', -// accessType: 'offline', -// prompt: 'consent', -// state: expect.any(String), -// }); -// // expect(spyRedirect).toBeCalledTimes(1); -// // expect(spyPassport).toBeCalledTimes(1); -// }); - -// it('should set a nonce cookie', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// query: { -// scope: 'a,b', -// }, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); -// googleAuthProvider.start(mockRequest, mockResponse); -// expect(mockResponse.cookie).toBeCalledTimes(1); -// expect(mockResponse.cookie).toBeCalledWith( -// 'google-nonce', -// expect.any(String), -// expect.objectContaining({ -// maxAge: TEN_MINUTES_MS, -// path: `/auth/${googleAuthProviderConfig.provider}/handler`, -// }), -// ); -// }); - -// it('should throw error if no scopes provided', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// query: {}, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); -// expect(() => { -// googleAuthProvider.start(mockRequest, mockResponse); -// }).toThrowError('missing scope parameter'); -// }); -// }); - -// describe('logout handler', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// } as unknown) as express.Request; - -// it('should perform logout and respond with 200', () => { -// const mockResponse: any = ({ -// send: jest.fn(), -// cookie: jest.fn(), -// } as unknown) as express.Response; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// const spyResponse = jest -// .spyOn(mockResponse, 'send') -// .mockImplementation(() => jest.fn()); - -// googleAuthProvider.logout(mockRequest, mockResponse); -// expect(spyResponse).toBeCalledTimes(1); -// expect(spyResponse).toBeCalledWith('logout!'); -// expect(mockResponse.cookie).toBeCalledTimes(1); -// expect(mockResponse.cookie).toBeCalledWith( -// 'google-refresh-token', -// '', -// expect.objectContaining({ maxAge: 0 }), -// ); -// }); -// }); - -// describe('redirect frame handler', () => { -// const mockResponse: any = ({ -// status: jest.fn().mockReturnThis(), -// send: jest.fn().mockReturnThis(), -// cookie: jest.fn().mockReturnThis(), -// } as unknown) as express.Response; - -// it('should call authenticate and post a response', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-nonce': 'NONCE' }, -// query: { -// state: 'NONCE', -// }, -// } as unknown) as express.Request; - -// const spyPostMessage = jest -// .spyOn(utils, 'postMessageResponse') -// .mockImplementation(() => jest.fn()); - -// const spyPassport = jest -// .spyOn(passport, 'authenticate') -// .mockImplementation((_x, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(null, { refreshToken: 'REFRESH_TOKEN' }); -// return jest.fn(); -// }); - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(spyPassport).toBeCalledTimes(1); -// expect(spyPostMessage).toBeCalledTimes(1); -// expect(mockResponse.cookie).toBeCalledTimes(1); -// expect(mockResponse.cookie).toBeCalledWith( -// 'google-refresh-token', -// 'REFRESH_TOKEN', -// expect.objectContaining({ -// path: '/auth/google', -// sameSite: 'none', -// httpOnly: true, -// maxAge: THOUSAND_DAYS_MS, -// }), -// ); -// }); - -// it('should respond with a error message if no refresh token returned', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-nonce': 'NONCE' }, -// query: { -// state: 'NONCE', -// }, -// } as unknown) as express.Request; - -// const spyPassport = jest -// .spyOn(passport, 'authenticate') -// .mockImplementation((_x, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(null, {}); -// return jest.fn(); -// }); - -// const spyPostMessage = jest -// .spyOn(utils, 'postMessageResponse') -// .mockImplementation(() => jest.fn()); - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(spyPassport).toBeCalledTimes(1); -// expect(spyPostMessage).toBeCalledTimes(1); -// expect(spyPostMessage).toBeCalledWith(mockResponse, { -// type: 'auth-result', -// error: new Error('Missing refresh token'), -// }); -// }); - -// it('should respond with a error message if auth failed', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-nonce': 'NONCE' }, -// query: { -// state: 'NONCE', -// }, -// } as unknown) as express.Request; - -// const spyPassport = jest -// .spyOn(passport, 'authenticate') -// .mockImplementation((_x, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(new Error('TokenError'), null); -// return jest.fn(); -// }); - -// const spyPostMessage = jest -// .spyOn(utils, 'postMessageResponse') -// .mockImplementation(() => jest.fn()); - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(spyPassport).toBeCalledTimes(1); -// expect(spyPostMessage).toBeCalledTimes(1); -// expect(spyPostMessage).toBeCalledWith(mockResponse, { -// type: 'auth-result', -// error: new Error('Google auth failed, Error: TokenError'), -// }); -// }); - -// it('should respond with a error message if cookie nonce is missing', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: {}, -// query: { state: 'NONCE' }, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith('Missing nonce'); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// }); - -// it('should respond with a error message if state nonce is missing', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-nonce': 'NONCE' }, -// query: {}, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith('Missing nonce'); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// }); - -// it('should respond with a error message if nonce mismatch', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-nonce': 'NONCA' }, -// query: { state: 'NONCEB' }, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.frameHandler(mockRequest, mockResponse); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith('Invalid nonce'); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// }); -// }); - -// describe('strategy handler', () => { -// it('should return a valid passport strategy', () => { -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// expect(googleAuthProvider.strategy()).toBeInstanceOf(passport.Strategy); -// }); - -// it('should throw an error for invalid options', () => { -// expect(() => { -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfigInvalidOptions, -// ); -// googleAuthProvider.strategy(); -// }).toThrow(); -// }); -// }); - -// describe('refresh token handler', () => { -// const mockResponse = ({ -// status: jest.fn().mockReturnThis(), -// send: jest.fn().mockReturnThis(), -// } as unknown) as express.Response; - -// describe('no refresh token cookie', () => { -// it('should respond with a 401', () => { -// const mockRequest = ({ -// cookies: jest.fn(), -// header: () => 'XMLHttpRequest', -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// googleAuthProvider.refresh(mockRequest, mockResponse); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith('Missing session cookie'); - -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// }); -// }); - -// describe('refresh token cookie, no scope', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, -// query: {}, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// it('should request for a new access token and fail if no access token returned', () => { -// const spyRefresh = jest -// .spyOn(refresh, 'requestNewAccessToken') -// .mockImplementation((_x, _y, _z, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(undefined, undefined, undefined, {}); -// }); - -// googleAuthProvider.refresh(mockRequest, mockResponse); -// expect(spyRefresh).toBeCalledTimes(1); -// expect(spyRefresh).toBeCalledWith( -// 'google', -// 'REFRESH_TOKEN', -// {}, -// expect.any(Function), -// ); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith( -// 'Failed to refresh access token', -// ); -// }); - -// it('should request for a new access token and return 401 if any error', () => { -// const spyRefresh = jest -// .spyOn(refresh, 'requestNewAccessToken') -// .mockImplementation((_x, _y, _z, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb({ error: 'ERROR' }, undefined, undefined, {}); -// }); - -// googleAuthProvider.refresh(mockRequest, mockResponse); -// expect(spyRefresh).toBeCalledTimes(1); -// expect(spyRefresh).toBeCalledWith( -// 'google', -// 'REFRESH_TOKEN', -// {}, -// expect.any(Function), -// ); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith( -// 'Failed to refresh access token', -// ); -// }); - -// it('should fetch and return a new access token', () => { -// const spyRefresh = jest -// .spyOn(refresh, 'requestNewAccessToken') -// .mockImplementation((_x, _y, _z, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(undefined, 'ACCESS_TOKEN', undefined, { -// expires_in: 'EXPIRES_IN', -// id_token: 'ID_TOKEN', -// }); -// }); - -// googleAuthProvider.refresh(mockRequest, mockResponse); -// expect(spyRefresh).toBeCalledTimes(1); -// expect(spyRefresh).toBeCalledWith( -// 'google', -// 'REFRESH_TOKEN', -// {}, -// expect.any(Function), -// ); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith({ -// accessToken: 'ACCESS_TOKEN', -// idToken: 'ID_TOKEN', -// expiresInSeconds: 'EXPIRES_IN', -// scope: undefined, -// }); -// }); -// }); - -// describe('refresh token cookie and scope', () => { -// const mockRequest = ({ -// header: () => 'XMLHttpRequest', -// cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, -// query: { -// scope: 'a,b', -// }, -// } as unknown) as express.Request; - -// const googleAuthProvider = new GoogleAuthProvider( -// googleAuthProviderConfig, -// ); - -// it('should fetch and return a new access token with scopes', () => { -// const spyRefresh = jest -// .spyOn(refresh, 'requestNewAccessToken') -// .mockImplementation((_x, _y, _z, callbackFunc) => { -// const cb = callbackFunc as Function; -// cb(undefined, 'ACCESS_TOKEN', undefined, { -// expires_in: 'EXPIRES_IN', -// id_token: 'ID_TOKEN', -// scope: 'a,b', -// }); -// }); - -// googleAuthProvider.refresh(mockRequest, mockResponse); -// expect(spyRefresh).toBeCalledTimes(1); -// expect(spyRefresh).toBeCalledWith( -// 'google', -// 'REFRESH_TOKEN', -// { scope: 'a,b' }, -// expect.any(Function), -// ); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith({ -// accessToken: 'ACCESS_TOKEN', -// idToken: 'ID_TOKEN', -// expiresInSeconds: 'EXPIRES_IN', -// scope: 'a,b', -// }); -// }); - -// it('ensures x-requested-with header', () => { -// const mockHeaderRequest = ({ -// header: () => 'TEST', -// } as unknown) as express.Request; - -// googleAuthProvider.refresh(mockHeaderRequest, mockResponse); -// expect(mockResponse.send).toBeCalledTimes(1); -// expect(mockResponse.send).toBeCalledWith( -// 'Invalid X-Requested-With header', -// ); -// expect(mockResponse.status).toBeCalledTimes(1); -// expect(mockResponse.status).toBeCalledWith(401); -// }); -// }); -// }); -// }); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index a090f7f950..bfa6ac7a81 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -1,151 +1,68 @@ -// /* -// * Copyright 2020 Spotify AB -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ +import express from 'express'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, +} from '../PassportStrategyHelper'; +import { + OAuthProviderHandlers, + AuthInfoBase, + AuthInfoPrivate, + RedirectInfo, + AuthProviderConfig, +} from '../types'; -// import passport from 'passport'; -// import express from 'express'; -// import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -// import { -// AuthProvider, -// AuthProviderRouteHandlers, -// AuthProviderConfig, -// } from './../types'; -// import { postMessageResponse, ensuresXRequestedWith } from './../utils'; -// import { InputError } from '@backstage/backend-common'; +export class GoogleAuthProvider implements OAuthProviderHandlers { + private readonly provider: string; + private readonly providerConfig: AuthProviderConfig; + private readonly _strategy: GoogleStrategy; -// export class GoogleAuthProvider -// implements AuthProvider, AuthProviderRouteHandlers { -// private readonly provider: string; -// private readonly providerConfig: AuthProviderConfig; -// private readonly _strategy: GoogleStrategy; + constructor(providerConfig: AuthProviderConfig) { + this.provider = providerConfig.provider; + this.providerConfig = providerConfig; + // TODO: throw error if env variables not set? + this._strategy = new GoogleStrategy( + { ...this.providerConfig.options }, + ( + accessToken: any, + refreshToken: any, + params: any, + profile: any, + done: any, + ) => { + done( + undefined, + { + profile, + idToken: params.id_token, + accessToken, + scope: params.scope, + expiresInSeconds: 10, + }, + { + refreshToken, + }, + ); + }, + ); + } -// constructor(handler: OAuthProviderHandlers) { -// this.provider = providerConfig.provider; -// this.providerConfig = providerConfig; -// // TODO: throw error if env variables not set? -// this._strategy = new GoogleStrategy( -// { ...this.providerConfig.options }, -// ( -// accessToken: any, -// refreshToken: any, -// params: any, -// profile: any, -// done: any, -// ) => { -// done( -// undefined, -// { -// profile, -// idToken: params.id_token, -// accessToken, -// scope: params.scope, -// expiresInSeconds: params.expires_in, -// }, -// { -// refreshToken, -// }, -// ); -// }, -// ); -// } + async start(req: express.Request, options: any): Promise { + return await executeRedirectStrategy(req, this._strategy, options); + } -// async start(req: express.Request, res: express.Response) { -// const scope = req.query.scope?.toString() ?? ''; + async handler( + req: express.Request, + ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { + return await executeFrameHandlerStrategy(req, this._strategy); + } -// if (!scope) { -// throw new InputError('missing scope parameter'); -// } - -// // router -> [AuthProviderRouteHandlers] -> OAuthProvider -> [OAuthProviderHandler] -> GoogleAuthProvider -// // router -> [AuthProviderRouteHandlers] -> GoogleAuthProvider - -// // class GoogleAuthProvider2 implements OAuthProviderHandler { -// // async start(req: express.Request): Promise { - -// // } -// // async handler(req: express.Request): Promise { -// // const { user, info } = await executeFrameHandlerStrategy( -// // req, -// // this.provider, -// // this._strategy, -// // ); -// // return { user, info } -// // } -// // } - -// executeRedirectStrategy(req, res, this.provider, this._strategy, { -// scope, -// accessType: 'offline', -// prompt: 'consent', -// }); -// } - -// async frameHandler(req: express.Request, res: express.Response) { -// try { -// // const { user, info } = await this.handler.handler(req); -// const { user, info } = await executeFrameHandlerStrategy(req); - -// const { refreshToken } = info; -// if (!refreshToken) { -// throw new Error('Missing refresh token'); -// } - -// setRefreshTokenCookie(res, this.provider, refreshToken); - -// return postMessageResponse(res, { -// type: 'auth-result', -// payload: user, -// }); -// } catch (error) { -// return postMessageResponse(res, { -// type: 'auth-result', -// error: { -// name: error.name, -// message: error.message, -// }, -// }); -// } -// } - -// async logout(req: express.Request, res: express.Response) { -// if (!ensuresXRequestedWith(req)) { -// return res.status(401).send('Invalid X-Requested-With header'); -// } - -// removeRefreshTokenCookie(res, this.provider); -// return res.send('logout!'); -// } - -// async refresh(req: express.Request, res: express.Response) { -// if (!ensuresXRequestedWith(req)) { -// return res.status(401).send('Invalid X-Requested-With header'); -// } - -// try { -// const refreshInfo = await executeRefreshTokenStrategy( -// req, -// this.provider, -// this._strategy, -// ); -// res.send(refreshInfo); -// } catch (error) { -// res.status(401).send(`${error.message}`); -// } -// } - -// strategy(): passport.Strategy { -// return this._strategy; -// } -// } + async refresh(refreshToken: string, scope: string): Promise { + return await executeRefreshTokenStrategy( + this._strategy, + refreshToken, + scope, + ); + } +} diff --git a/plugins/auth-backend/src/providers/index.test.ts b/plugins/auth-backend/src/providers/index.test.ts deleted file mode 100644 index 74f7657c44..0000000000 --- a/plugins/auth-backend/src/providers/index.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -// /* -// * Copyright 2020 Spotify AB -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ - -// import passport from 'passport'; -// import express from 'express'; -// import { makeProvider, defaultRouter } from '.'; -// import { AuthProviderRouteHandlers, AuthProviderConfig } from './types'; -// import * as passportGoogleOAuth20 from 'passport-google-oauth20'; -// import { ProviderFactories } from './factories'; - -// class MyOAuthProvider implements AuthProviderHandlers {} - -// class MyAuthProvider implements AuthProviderRouteHandlers { -// // private readonly providerConfig: AuthProviderConfig; -// constructor(providerConfig: AuthProviderConfig) { -// this.providerConfig = providerConfig; -// } - -// strategy(): passport.Strategy { -// return new passportGoogleOAuth20.Strategy( -// this.providerConfig.options, -// () => {}, -// ); -// } -// async start(_: express.Request, res: express.Response): Promise { -// res.send('start'); -// } -// async frameHandler(_: express.Request, res: express.Response): Promise { -// res.send('frameHandler'); -// } -// async logout(_: express.Request, res: express.Response): Promise { -// res.send('logout'); -// } -// } - -// class MyAuthProviderWithRefresh extends MyAuthProvider { -// async refresh(_: express.Request, res: express.Response): Promise { -// res.send('logout'); -// } -// } - -// const providerConfig = { -// provider: 'a', -// options: { -// clientID: 'somevalue', -// }, -// }; - -// const providerConfigInvalid = { -// provider: 'b', -// options: { -// clientID: 'somevalue', -// }, -// }; - -// describe('makeProvider', () => { -// it('makes a provider for Myauthprovider', () => { -// jest -// .spyOn(ProviderFactories, 'getProviderFactory') -// .mockReturnValueOnce(MyAuthProvider); -// const provider = makeProvider(providerConfig); -// expect(provider.providerId).toEqual('a'); -// expect(provider.providerRouter).toBeDefined(); -// }); - -// it('throws an error when provider implementation does not exist', () => { -// expect(() => { -// makeProvider(providerConfigInvalid); -// }).toThrow('Provider Implementation missing for : b auth provider'); -// }); -// }); - -// describe('defaultRouter', () => { -// it('make router for auth provider without refresh', () => { -// expect( -// defaultRouter(new MyAuthProvider({ provider: 'a', options: {} })), -// ).toBeDefined(); -// }); - -// it('make router for auth provider with refresh', () => { -// expect( -// defaultRouter( -// new MyAuthProviderWithRefresh({ provider: 'b', options: {} }), -// ), -// ).toBeDefined(); -// }); -// }); diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index a9c43716e2..6c4bf82481 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -26,34 +26,14 @@ export interface OAuthProviderHandlers { start(req: express.Request, options: any): Promise; handler(req: express.Request): Promise; refresh(refreshToken: string, scope: string): Promise; - logout( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; + logout?(): Promise; } export interface AuthProviderRouteHandlers { - start( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; - frameHandler( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; - refresh?( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; - logout( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ): Promise; + start(req: express.Request, res: express.Response): Promise; + frameHandler(req: express.Request, res: express.Response): Promise; + refresh?(req: express.Request, res: express.Response): Promise; + logout(req: express.Request, res: express.Response): Promise; } export type AuthProviderFactories = { From 643e4e17fcc9dfc5b5c0bec9ee34e279db9bc489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 21:46:27 +0200 Subject: [PATCH 23/97] Add default namespace if none was given --- packages/catalog-model/src/EntityPolicies.ts | 2 + .../DefaultNamespaceEntityPolicy.test.ts | 61 +++++++++++++++++++ .../policies/DefaultNamespaceEntityPolicy.ts | 38 ++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts create mode 100644 packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index fa25334d5f..ca189c042e 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -23,6 +23,7 @@ import { } from './entity'; import { ComponentV1beta1Policy } from './kinds'; import { EntityPolicy } from './types'; +import { DefaultNamespaceEntityPolicy } from './entity/policies/DefaultNamespaceEntityPolicy'; // Helper that requires that all of a set of policies can be successfully // applied @@ -62,6 +63,7 @@ export class EntityPolicies implements EntityPolicy { return EntityPolicies.allOf([ EntityPolicies.allOf([ new SchemaValidEntityPolicy(), + new DefaultNamespaceEntityPolicy(), new NoForeignRootFieldsEntityPolicy(), new FieldFormatEntityPolicy(), new ReservedFieldsEntityPolicy(), diff --git a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts new file mode 100644 index 0000000000..68658e5296 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; +import { DefaultNamespaceEntityPolicy } from './DefaultNamespaceEntityPolicy'; + +describe('DefaultNamespaceEntityPolicy', () => { + let withNamespace: any; + let withoutNamespace: any; + let policy: DefaultNamespaceEntityPolicy; + + beforeEach(() => { + withoutNamespace = yaml.parse(` + apiVersion: backstage.io/v1beta1 + kind: Component + metadata: + name: my-component-yay + `); + withNamespace = yaml.parse(` + apiVersion: backstage.io/v1beta1 + kind: Component + metadata: + name: my-component-yay + namespace: my-home + `); + policy = new DefaultNamespaceEntityPolicy(); + }); + + it('leaves untouched if it already has a namespace', async () => { + const result = policy.enforce(withNamespace); + await expect(result).resolves.toBe(withNamespace); + await expect(result).resolves.toEqual( + expect.objectContaining({ + metadata: { name: 'my-component-yay', namespace: 'my-home' }, + }), + ); + }); + + it('adds namespace in different object if it did not have one', async () => { + const result = policy.enforce(withoutNamespace); + await expect(result).resolves.not.toBe(withoutNamespace); + await expect(result).resolves.toEqual( + expect.objectContaining({ + metadata: { name: 'my-component-yay', namespace: 'default' }, + }), + ); + }); +}); diff --git a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts new file mode 100644 index 0000000000..e5aba745c7 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import lodash from 'lodash'; +import { EntityPolicy } from '../../types'; +import { Entity } from '../Entity'; + +/** + * Sets a default namespace if none was set. + */ +export class DefaultNamespaceEntityPolicy implements EntityPolicy { + private readonly namespace: string; + + constructor(namespace: string = 'default') { + this.namespace = namespace; + } + + async enforce(entity: Entity): Promise { + if (entity.metadata.namespace) { + return entity; + } + + return lodash.merge({ metadata: { namespace: this.namespace } }, entity); + } +} From c35c9d8023742516a60d042953ee53409a3a3011 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Thu, 28 May 2020 21:55:39 +0200 Subject: [PATCH 24/97] remove unused packages --- plugins/auth-backend/package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index fe82e544e7..9cb8017f15 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -27,14 +27,10 @@ "yn": "^4.0.0", "passport": "^0.4.1", "passport-google-oauth20": "^2.0.0", - "passport-oauth2-refresh": "^2.0.0", - "passport-oauth2": "^1.5.0", "cookie-parser": "^1.4.5", - "@types/passport-oauth2-refresh": "^1.1.1", "@types/passport": "^1.0.3", "@types/passport-google-oauth20": "^2.0.3", - "@types/cookie-parser": "^1.4.2", - "@types/passport-oauth2": "^1.4.9" + "@types/cookie-parser": "^1.4.2" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", From 267ec74f2a5822f19d72ac676bf971268a635242 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 29 May 2020 08:28:27 +0200 Subject: [PATCH 25/97] build(deps-dev): bump ts-node from 8.8.1 to 8.10.2 (#1065) Bumps [ts-node](https://github.com/TypeStrong/ts-node) from 8.8.1 to 8.10.2. - [Release notes](https://github.com/TypeStrong/ts-node/releases) - [Commits](https://github.com/TypeStrong/ts-node/compare/v8.8.1...v8.10.2) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 74b9e451b0..8d51ddee0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18995,10 +18995,10 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.6, source-map-support@~0.5.12: - version "0.5.16" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== +source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12: + version "0.5.19" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -20323,14 +20323,14 @@ ts-loader@^7.0.4: semver "^6.0.0" ts-node@^8.6.2: - version "8.8.1" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.8.1.tgz#7c4d3e9ed33aa703b64b28d7f9d194768be5064d" - integrity sha512-10DE9ONho06QORKAaCBpPiFCdW+tZJuY/84tyypGtl6r+/C7Asq0dhqbRZURuUlLQtZxxDvT8eoj8cGW0ha6Bg== + version "8.10.2" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" + integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== dependencies: arg "^4.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.6" + source-map-support "^0.5.17" yn "3.1.1" ts-pnp@^1.1.2: From d16dfdc55bcf1c707d950331260dd8edd0e88830 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Thu, 28 May 2020 22:06:53 +0200 Subject: [PATCH 26/97] move refresh token response to provider --- .../src/providers/OAuthProvider.ts | 146 ++++++++++-------- .../src/providers/PassportStrategyHelper.ts | 28 +++- .../src/providers/google/index.ts | 16 ++ .../src/providers/google/provider.ts | 29 +++- plugins/auth-backend/src/providers/types.ts | 5 + plugins/auth-backend/src/service/router.ts | 1 + 6 files changed, 149 insertions(+), 76 deletions(-) diff --git a/plugins/auth-backend/src/providers/OAuthProvider.ts b/plugins/auth-backend/src/providers/OAuthProvider.ts index d02f7da110..81f5ed25a6 100644 --- a/plugins/auth-backend/src/providers/OAuthProvider.ts +++ b/plugins/auth-backend/src/providers/OAuthProvider.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import express, { CookieOptions } from 'express'; import crypto from 'crypto'; import { AuthProviderRouteHandlers, OAuthProviderHandlers } from './types'; @@ -7,6 +23,69 @@ import { postMessageResponse, ensuresXRequestedWith } from './utils'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; +export const verifyNonce = (req: express.Request, provider: string) => { + const cookieNonce = req.cookies[`${provider}-nonce`]; + const stateNonce = req.query.state; + + if (!cookieNonce || !stateNonce) { + throw new Error('Missing nonce'); + } + + if (cookieNonce !== stateNonce) { + throw new Error('Invalid nonce'); + } +}; + +export const setNonceCookie = (res: express.Response, provider: string) => { + const nonce = crypto.randomBytes(16).toString('base64'); + + const options: CookieOptions = { + maxAge: TEN_MINUTES_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}/handler`, + httpOnly: true, + }; + + res.cookie(`${provider}-nonce`, nonce, options); + + return nonce; +}; + +export const setRefreshTokenCookie = ( + res: express.Response, + provider: string, + refreshToken: string, +) => { + const options: CookieOptions = { + maxAge: THOUSAND_DAYS_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, refreshToken, options); +}; + +export const removeRefreshTokenCookie = ( + res: express.Response, + provider: string, +) => { + const options: CookieOptions = { + maxAge: 0, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${provider}`, + httpOnly: true, + }; + + res.cookie(`${provider}-refresh-token`, '', options); +}; + export class OAuthProvider implements AuthProviderRouteHandlers { private readonly provider: string; private readonly providerHandlers: OAuthProviderHandlers; @@ -106,72 +185,9 @@ export class OAuthProvider implements AuthProviderRouteHandlers { refreshToken, scope, ); - res.send(refreshInfo); + return res.send(refreshInfo); } catch (error) { - res.status(401).send(`${error.message}`); + return res.status(401).send(`${error.message}`); } } } - -export const verifyNonce = (req: express.Request, provider: string) => { - const cookieNonce = req.cookies[`${provider}-nonce`]; - const stateNonce = req.query.state; - - if (!cookieNonce || !stateNonce) { - throw new Error('Missing nonce'); - } - - if (cookieNonce !== stateNonce) { - throw new Error('Invalid nonce'); - } -}; - -export const setNonceCookie = (res: express.Response, provider: string) => { - const nonce = crypto.randomBytes(16).toString('base64'); - - const options: CookieOptions = { - maxAge: TEN_MINUTES_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}/handler`, - httpOnly: true, - }; - - res.cookie(`${provider}-nonce`, nonce, options); - - return nonce; -}; - -export const setRefreshTokenCookie = ( - res: express.Response, - provider: string, - refreshToken: string, -) => { - const options: CookieOptions = { - maxAge: THOUSAND_DAYS_MS, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}`, - httpOnly: true, - }; - - res.cookie(`${provider}-refresh-token`, refreshToken, options); -}; - -export const removeRefreshTokenCookie = ( - res: express.Response, - provider: string, -) => { - const options: CookieOptions = { - maxAge: 0, - secure: false, - sameSite: 'none', - domain: 'localhost', - path: `/auth/${provider}`, - httpOnly: true, - }; - - res.cookie(`${provider}-refresh-token`, '', options); -}; diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts index 515a91f25c..3ec8539330 100644 --- a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts @@ -1,6 +1,22 @@ -import express, { CookieOptions } from 'express'; +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; import passport from 'passport'; -import { RedirectInfo, AuthInfoBase } from './types'; +import { RedirectInfo, RefreshTokenResponse } from './types'; export const executeRedirectStrategy = async ( req: express.Request, @@ -28,7 +44,7 @@ export const executeFrameHandlerStrategy = async ( }; strategy.fail = ( info: { type: 'success' | 'error'; message?: string }, - _status?: number, + // _status: number, ) => { reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); }; @@ -47,7 +63,7 @@ export const executeRefreshTokenStrategy = async ( providerstrategy: passport.Strategy, refreshToken: string, scope: string, -): Promise => { +): Promise => { return new Promise((resolve, reject) => { const anyStrategy = providerstrategy as any; const OAuth2 = anyStrategy._oauth2.constructor; @@ -84,9 +100,7 @@ export const executeRefreshTokenStrategy = async ( } resolve({ accessToken, - idToken: params.id_token, - expiresInSeconds: 10, - scope: params.scope, + params, }); }, ); diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index 5a94d88ada..0ec98bef89 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -1 +1,17 @@ +/* + * 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. + */ + export { GoogleAuthProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index bfa6ac7a81..90d33652a5 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import express from 'express'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { @@ -14,12 +30,10 @@ import { } from '../types'; export class GoogleAuthProvider implements OAuthProviderHandlers { - private readonly provider: string; private readonly providerConfig: AuthProviderConfig; private readonly _strategy: GoogleStrategy; constructor(providerConfig: AuthProviderConfig) { - this.provider = providerConfig.provider; this.providerConfig = providerConfig; // TODO: throw error if env variables not set? this._strategy = new GoogleStrategy( @@ -38,7 +52,7 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { idToken: params.id_token, accessToken, scope: params.scope, - expiresInSeconds: 10, + expiresInSeconds: params.expires_in, }, { refreshToken, @@ -59,10 +73,17 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { } async refresh(refreshToken: string, scope: string): Promise { - return await executeRefreshTokenStrategy( + const { accessToken, params } = await executeRefreshTokenStrategy( this._strategy, refreshToken, scope, ); + + return { + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }; } } diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 6c4bf82481..dcbe6ad1de 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -73,3 +73,8 @@ export type RedirectInfo = { url: string; status?: number; }; + +export type RefreshTokenResponse = { + accessToken: string; + params: any; +}; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 03d197f50b..49487d551a 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -36,6 +36,7 @@ export async function createRouter( // configure all the providers for (const providerConfig of providers) { const { providerId, providerRouter } = makeProvider(providerConfig); + logger.info(`Configuring provider, ${providerId}`); router.use(`/${providerId}`, providerRouter); } From 3dd246801e5d4925c1e92200fa437a143041b036 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 29 May 2020 09:19:25 +0200 Subject: [PATCH 27/97] build(deps): bump material-table from 1.58.0 to 1.58.2 (#1067) Bumps [material-table](https://github.com/mbrn/material-table) from 1.58.0 to 1.58.2. - [Release notes](https://github.com/mbrn/material-table/releases) - [Commits](https://github.com/mbrn/material-table/commits) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8d51ddee0d..1e5dc6ab2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14189,9 +14189,9 @@ marked@^0.8.0: integrity sha512-tZfJS8uE0zpo7xpTffwFwYRfW9AzNcdo04Qcjs+C9+oCy8MSRD2reD5iDVtYx8mtLaqsGughw/YLlcwNxAHA1g== material-table@^1.58.0: - version "1.58.0" - resolved "https://registry.npmjs.org/material-table/-/material-table-1.58.0.tgz#1902f88b74436ce880b234c7c713f0e34a7e2f9d" - integrity sha512-5xiiKERNZv9Zai2TMT5d9LRLrKlofAFe2YZZzxP7cJ9DIZC8HcaflunyIVHYTzK1yFS51TqjDvHIqxYLIqdahQ== + version "1.58.2" + resolved "https://registry.npmjs.org/material-table/-/material-table-1.58.2.tgz#dc0d19652848e6bb92f747d122bd7d4681cca6dc" + integrity sha512-s/m6ebyXFXmg07zxv1Fl6qPySKaiQhASXaOB3ubRKUFA1DkryUy3PGSEVWTjUYnRyi63kGq+N6b5nsokLR6m5A== dependencies: "@date-io/date-fns" "^1.1.0" "@material-ui/pickers" "^3.2.2" From 95c92a52aae25f70686251e1636a0bf656cc343c Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Fri, 29 May 2020 09:21:25 +0200 Subject: [PATCH 28/97] unbreak the test runner --- .../auth-backend/src/providers/index.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 plugins/auth-backend/src/providers/index.test.ts diff --git a/plugins/auth-backend/src/providers/index.test.ts b/plugins/auth-backend/src/providers/index.test.ts new file mode 100644 index 0000000000..b3e2f19771 --- /dev/null +++ b/plugins/auth-backend/src/providers/index.test.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +describe('test', () => { + it('unbreaks the test runner', () => { + expect(true).toBeTruthy(); + }); +}); From 98401ed99bef3bef9c26a994319bd5bd76db80ff Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 29 May 2020 09:28:37 +0200 Subject: [PATCH 29/97] build(deps-dev): bump @storybook/addons from 5.3.18 to 5.3.19 (#1039) Bumps [@storybook/addons](https://github.com/storybookjs/storybook/tree/HEAD/lib/addons) from 5.3.18 to 5.3.19. - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v5.3.19/lib/addons) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 1e5dc6ab2b..8439f5b0b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3071,7 +3071,7 @@ regenerator-runtime "^0.13.3" util-deprecate "^1.0.2" -"@storybook/addons@5.3.18", "@storybook/addons@^5.3.17": +"@storybook/addons@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-5.3.18.tgz#5cbba6407ef7a802041c5ee831473bc3bed61f64" integrity sha512-ZQjDgTUDFRLvAiBg2d8FgPgghfQ+9uFyXQbtiGlTBLinrPCeQd7J86qiUES0fcGoohCCw0wWKtvB0WF2z1XNDg== @@ -3084,6 +3084,19 @@ global "^4.3.2" util-deprecate "^1.0.2" +"@storybook/addons@^5.3.17": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-5.3.19.tgz#3a7010697afd6df9a41b8c8a7351d9a06ff490a4" + integrity sha512-Ky/k22p6i6FVNvs1VhuFyGvYJdcp+FgXqFgnPyY/OXJW/vPDapdElpTpHJZLFI9I2FQBDcygBPU5RXkumQ+KUQ== + dependencies: + "@storybook/api" "5.3.19" + "@storybook/channels" "5.3.19" + "@storybook/client-logger" "5.3.19" + "@storybook/core-events" "5.3.19" + core-js "^3.0.1" + global "^4.3.2" + util-deprecate "^1.0.2" + "@storybook/api@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/api/-/api-5.3.18.tgz#95582ab90d947065e0e34ed603650a3630dcbd16" @@ -3110,6 +3123,32 @@ telejson "^3.2.0" util-deprecate "^1.0.2" +"@storybook/api@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/api/-/api-5.3.19.tgz#77f15e9e2eee59fe1ddeaba1ef39bc34713a6297" + integrity sha512-U/VzDvhNCPmw2igvJYNNM+uwJCL+3teiL6JmuoL4/cmcqhI6IqqG9dZmMP1egoCd19wXEP7rnAfB/VcYVg41dQ== + dependencies: + "@reach/router" "^1.2.1" + "@storybook/channels" "5.3.19" + "@storybook/client-logger" "5.3.19" + "@storybook/core-events" "5.3.19" + "@storybook/csf" "0.0.1" + "@storybook/router" "5.3.19" + "@storybook/theming" "5.3.19" + "@types/reach__router" "^1.2.3" + core-js "^3.0.1" + fast-deep-equal "^2.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + prop-types "^15.6.2" + react "^16.8.3" + semver "^6.0.0" + shallow-equal "^1.1.0" + store2 "^2.7.1" + telejson "^3.2.0" + util-deprecate "^1.0.2" + "@storybook/channel-postmessage@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-5.3.18.tgz#93d46740b5cc9b36ddd073f0715b54c4959953bf" @@ -3128,6 +3167,13 @@ dependencies: core-js "^3.0.1" +"@storybook/channels@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-5.3.19.tgz#65ad7cd19d70aa5eabbb2e5e39ceef5e510bcb7f" + integrity sha512-38seaeyshRGotTEZJppyYMg/Vx2zRKgFv1L6uGqkJT0LYoNSYtJhsiNFCJ2/KUJu2chAJ/j8h80bpVBVLQ/+WA== + dependencies: + core-js "^3.0.1" + "@storybook/client-api@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-5.3.18.tgz#e71041796f95888de0e4524734418e6b120b060a" @@ -3158,6 +3204,13 @@ dependencies: core-js "^3.0.1" +"@storybook/client-logger@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-5.3.19.tgz#fbbd186e82102eaca1d6a5cca640271cae862921" + integrity sha512-nHftT9Ow71YgAd2/tsu79kwKk30mPuE0sGRRUHZVyCRciGFQweKNOS/6xi2Aq+WwBNNjPKNlbgxwRt1yKe1Vkg== + dependencies: + core-js "^3.0.1" + "@storybook/components@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/components/-/components-5.3.18.tgz#528f6ab1660981e948993a04b407a6fad7751589" @@ -3192,6 +3245,13 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-5.3.19.tgz#18020cd52e0d8ef0973a8e9622a10d5f99796f79" + integrity sha512-lh78ySqMS7pDdMJAQAe35d1I/I4yPTqp09Cq0YIYOxx9BQZhah4DZTV1QIZt22H5p2lPb5MWLkWSxBaexZnz8A== + dependencies: + core-js "^3.0.1" + "@storybook/core@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/core/-/core-5.3.18.tgz#3f3c0498275826c1cc4368aba203ac17a6ae5c9c" @@ -3332,6 +3392,21 @@ qs "^6.6.0" util-deprecate "^1.0.2" +"@storybook/router@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/router/-/router-5.3.19.tgz#0f783b85658f99e4007f74347ad7ef17dbf7fc3a" + integrity sha512-yNClpuP7BXQlBTRf6Ggle3/R349/k6kvI5Aim4jf6X/2cFVg2pzBXDAF41imNm9PcvdxwabQLm6I48p7OvKr/w== + dependencies: + "@reach/router" "^1.2.1" + "@storybook/csf" "0.0.1" + "@types/reach__router" "^1.2.3" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + util-deprecate "^1.0.2" + "@storybook/source-loader@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-5.3.18.tgz#39ba28d9664ab8204d6b04ee757772369931e7e5" @@ -3366,6 +3441,24 @@ resolve-from "^5.0.0" ts-dedent "^1.1.0" +"@storybook/theming@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-5.3.19.tgz#177d9819bd64f7a1a6ea2f1920ffa5baf9a5f467" + integrity sha512-ecG+Rq3hc1GOzKHamYnD4wZ0PEP9nNg0mXbC3RhbxfHj+pMMCWWmx9B2Uu75SL1PTT8WcfkFO0hU/0IO84Pzlg== + dependencies: + "@emotion/core" "^10.0.20" + "@emotion/styled" "^10.0.17" + "@storybook/client-logger" "5.3.19" + core-js "^3.0.1" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.19" + global "^4.3.2" + memoizerific "^1.11.3" + polished "^3.3.1" + prop-types "^15.7.2" + resolve-from "^5.0.0" + ts-dedent "^1.1.0" + "@storybook/ui@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-5.3.18.tgz#c66f6d94a3c50bb706f4d5b1d5592439110f16f0" From d90913b028bc35ac11921689bc9094cf8600794e Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Fri, 29 May 2020 10:08:55 +0200 Subject: [PATCH 30/97] fix: split catalog api into types and implementation --- packages/app/src/apis.ts | 4 +- plugins/catalog/src/api/index.ts | 51 ----------- plugins/catalog/src/api/types.ts | 153 +++---------------------------- plugins/catalog/src/index.ts | 4 +- 4 files changed, 16 insertions(+), 196 deletions(-) delete mode 100644 plugins/catalog/src/api/index.ts diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 8efc3757eb..8f5198bd5a 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -38,7 +38,7 @@ import { import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar'; import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci'; -import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog'; +import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; const builder = ApiRegistry.builder(); @@ -74,7 +74,7 @@ builder.add( builder.add( catalogApiRef, - new CatalogApi({ + new CatalogClient({ apiOrigin: 'http://localhost:3000', basePath: '/catalog/api', }), diff --git a/plugins/catalog/src/api/index.ts b/plugins/catalog/src/api/index.ts deleted file mode 100644 index 83c0eb1cfd..0000000000 --- a/plugins/catalog/src/api/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createApiRef } from '@backstage/core'; -import { DescriptorEnvelope } from './types'; - -export const catalogApiRef = createApiRef({ - id: 'plugin.catalog.service', - description: - 'Used by the Catalog plugin to make requests to accompanying backend', -}); - -export class CatalogApi { - private apiOrigin: string; - private basePath: string; - constructor({ - apiOrigin, - basePath, - }: { - apiOrigin: string; - basePath: string; - }) { - this.apiOrigin = apiOrigin; - this.basePath = basePath; - } - async getEntities(): Promise { - const response = await fetch(`${this.apiOrigin}${this.basePath}/entities`); - return await response.json(); - } - async getEntityByName(name: string): Promise { - const response = await fetch( - `${this.apiOrigin}${this.basePath}/entities/by-name/Component/default/${name}`, - ); - const entity = await response.json(); - if (entity) return entity; - throw new Error(`'Entity not found: ${name}`); - } -} diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 0f91e1ed8e..6cc1dfd6cc 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -13,147 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { createApiRef } from '@backstage/core'; +import { DescriptorEnvelope } from '../types'; -export interface ComponentDescriptorV1beta1 extends DescriptorEnvelope { - spec: { - type: string; - }; +export const catalogApiRef = createApiRef({ + id: 'plugin.catalog.service', + description: + 'Used by the Catalog plugin to make requests to accompanying backend', +}); + +export interface CatalogApi { + getEntities(): Promise; + getEntityByName(name: string): Promise; } - -export type ComponentDescriptor = ComponentDescriptorV1beta1; - -/** - * Metadata fields common to all versions/kinds of entity. - * - * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta - */ -export type EntityMeta = { - /** - * A globally unique ID for the entity. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. The field can (optionally) be specified when performing - * update or delete operations, but the server is free to reject requests - * that do so in such a way that it breaks semantics. - */ - uid?: string; - - /** - * An opaque string that changes for each update operation to any part of - * the entity, including metadata. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. The field can (optionally) be specified when performing - * update or delete operations, and the server will then reject the - * operation if it does not match the current stored value. - */ - etag?: string; - - /** - * A positive nonzero number that indicates the current generation of data - * for this entity; the value is incremented each time the spec changes. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. - */ - generation?: number; - - /** - * The name of the entity. - * - * Must be uniqe within the catalog at any given point in time, for any - * given namespace, for any given kind. - */ - name: string; - - /** - * The short description of the entity. - * - * A a human readable string. - */ - description: string; - - /** - * The namespace that the entity belongs to. - */ - namespace?: string; - - /** - * Key/value pairs of identifying information attached to the entity. - */ - labels?: Record; - - /** - * Key/value pairs of non-identifying auxiliary information attached to the - * entity. - */ - annotations?: Record; -}; - -/** - * The format envelope that's common to all versions/kinds. - * - * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ - */ -export type DescriptorEnvelope = { - /** - * The version of specification format for this particular entity that - * this is written against. - */ - apiVersion: string; - - /** - * The high level entity type being described. - */ - kind: string; - - /** - * Optional metadata related to the entity. - */ - metadata: EntityMeta; - - /** - * The specification data describing the entity itself. - */ - spec?: object; -}; - -/** - * Parses and validates descriptors. - * - * The output must be validated and well formed. - */ -export type DescriptorParser = { - /** - * Parses and validates a single raw descriptor. - * - * @param descriptor A raw descriptor object - * @returns A structure describing the parsed and validated descriptor - * @throws An Error if the descriptor was malformed - */ - parse(descriptor: object): Promise; -}; - -/** - * Parses and validates a single envelope into its materialized kind. - * - * These parsers may assume that the envelope is already validated and well - * formed. - */ -export type KindParser = { - /** - * Try to parse an envelope into a materialized kind. - * - * @param envelope A valid descriptor envelope - * @returns A materialized type, or undefined if the given version/kind is - * not meant to be handled by this parser - * @throws An Error if the type was handled and found to not be properly - * formatted - */ - tryParse( - envelope: DescriptorEnvelope, - ): Promise; -}; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index d67bc6a864..f495bd3146 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -15,4 +15,6 @@ */ export { plugin } from './plugin'; -export * from './api'; +export * from './api/CatalogClient'; +export * from './api/types'; +export * from './types'; From 1312d34147af604355117ee85ed913e7a9460092 Mon Sep 17 00:00:00 2001 From: Marc Bruggmann Date: Fri, 29 May 2020 10:23:01 +0200 Subject: [PATCH 31/97] ADR: Core entities in the catalog This is capturing the catalog part of the Backstage System Model RFC (#390). I'm planning to follow this up with a second ADR on how to express System, Application and Domain in catalog labels. Not sure if it helps to have the example yamls inline here. It helps me to visualize how it'd work, but it could also get outdated quickly. --- .../adr005-catalog-core-entities.md | 75 ++++++++++++++++++ .../catalog-core-entities.png | Bin 0 -> 14895 bytes 2 files changed, 75 insertions(+) create mode 100644 docs/architecture-decisions/adr005-catalog-core-entities.md create mode 100644 docs/architecture-decisions/catalog-core-entities.png diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md new file mode 100644 index 0000000000..f14f8c8179 --- /dev/null +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -0,0 +1,75 @@ +# ADR005: Catalog Core Entities + +| Created | Status | +| ---------- | ------ | +| 2020-05-29 | Open | + +## Context + +We want to standardize on a few core entities that we are tracking in the Backstage catalog. This allows us to build specific plugins around them. + +## Decision + +We maintain a catalog of the following core entities: + +* **Components** are individual pieces of software +* **APIs** are the boundaries between different components +* **Resources** are physical or virtual infrastructure needed to operate a component + +![Catalog Core Entities][catalog-core-entities] + +### Component +A component is a piece of software, for example a mobile application feature, web site, backend service or data pipeline (list not exhaustive). A component can be tracked in source control, or use some existing open source or commercial software. It can implement APIs for other components to consume. In turn it might depend on APIs implemented by other components, or resources that are attached to it at runtime. + +Component entities are typically defined in YAML descriptor files next to the code of the component, and could look like this (actual schema will evolve): +```yaml +apiVersion: backstage.io/v1beta1 +kind: Component +metadata: + name: my-component-name +spec: + type: service +``` + +### API +APIs form an abstraction that allows large software ecosystems to scale. Thus, APIs are a first class citizen in the Backstage model and the primary way to discover existing functionality in the ecosystem. + +APIs are implemented by components and make their boundaries explicit. They might be defined using an RPC IDL (eg in Protobuf, GraphQL or similar), a data schema (eg in Avro, TFRecord or similar), or as code interfaces (eg framework APIs in Swift, Kotlin, Java, C++, Typescript etc). In any case, APIs exposed by components need to be in a known machine-readable format so we can build further tooling and analysis on top. + +APIs are typically indexed from existing definitions in source control and thus wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve): +```yaml +apiVersion: backstage.io/v1beta1 +kind: API +metadata: + name: my-component-api +spec: + type: grpc + definition: > + service HelloService { + rpc SayHello (HelloRequest) returns (HelloResponse); + } + message HelloRequest { + string greeting = 1; + } + message HelloResponse { + string reply = 1; + } +``` + +### Resource +Resources are the infrastructure your software needs to operate at runtime like Bigtable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and APIs will allow us to visualize and create tooling around them in Backstage. + +Resources are typically indexed from declarative definitions (eg Terraform, GCP Config Connector, AWS Cloud Formation) and/or inventories from cloud providers (eg GCP Asset Inventory) and thus wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve): +```yaml +apiVersion: backstage.io/v1beta1 +kind: Resource +metadata: + name: my-component-db +spec: + type: gcp-spanner + url: spanner.googleapis.com/projects/prj/instances/my-component-db/databases/my-db +``` + +## Consequences + +We will start with fleshing out support for the Component entity in the catalog, and expand to APIs and Resources later down the line. diff --git a/docs/architecture-decisions/catalog-core-entities.png b/docs/architecture-decisions/catalog-core-entities.png new file mode 100644 index 0000000000000000000000000000000000000000..b0c7cb4575e3426d298753c9bb626e45ca035e60 GIT binary patch literal 14895 zcmeHtXH=70+vWot@EjEN*pQ}Jks^pRslj#-5Kw8-RTPvegcd>)6uVLc0jWWyNR5Cr z69^uUbP}XR3?yg*5duUKl8`>Z_j|wj=FF^Fv*ypNS?lD-6IgrY*?ZsnF4w-V>)|

`=?tkU)}t0i*7yT^YN~f(>pUV52c>Sb~{jh{?W~AIu}~b-zN5)-lMqvvr^ov zZB4J6wy5hoay%XSXg&Yrxlj4y^2#fAC9M03w;y@P=T=SMBM4w$9K9Q9liZ~Nj2Va| zNV;UmBDHfT!HfYfC?l3L5!@szUDrRKe=P97v%s;Vq$q}RfOmJ=W`Y-n`u#L~D^rbG zu$ifLGfJrkhlS6%+-S&%`_Am7O#14<8uk~Oiv z?ZNI@d)!hu^{djaKVR|laozUL|M4(SX(;*E7c$4!7O^(n+DzANrLzbX)~5S8Q?l?N z)r@MkhiYakx7S$HZU{zS?XxHj=%&z*jqO5*Yq+F$cVN;izXb9G`{>7v{3T7XNkJNB zZ)uanW)amQB0SyNFg((PR9);!6b~ebjOeY`G+I!UiDmdEClo#u8G@JmSXoV>Cq!5k zb3uo(zwwkr|2bdxaRmZdRc;PVSpN24Y-bVzsu6Ip+nT;AT;(k-uWF4i&s%huD~zW< z3AeV`g++%877l4*n$xl<%iut@DO>`1Y-cIgE?R=JQ9B;dMpw_+*K6GYaEgR7P{0DJX?g z(X0npNsqB|ySl&1={u|ZL#Bm08J;6-gC^kIGfL=x;A> zIC=(tx$rkc?1mx6^+uVFBMQB=E~JVOIoSZ*jNb5r&W5B{ct-lgBZ4qwI>kIamF5>B zQ^IK(*XS|DHYbLDdS77o>BTMt>`TYrjQaRQ=V0~B>1F5CZfC!5&5@H7ww6Aa=;)jl zPHM#mTWkNzal0S?uLDOKUpP|5tC7z3erLkQdE|% z3#cc_Mfd$ac)l+ZD=t7l%s~4ig_EM-_$A4x!J@EdT?C93FZoUu!g!Z-a;WHDZfVLB zVVp=A$YfM)VlN{l-*NFU?n4&;+|QKesTaKilsZ()K#&Vkgii9wVnJb1AEQIEuERxU zdRjGOgK)QBcWB!6GzWOU-V2?bHk{E&WPFZg(YKg8AofH6h+(3As{Hzo=}5#CS}%h4 z7bG3Ftc%661d^5bxL|{8P!&B|^zHnJ0s?gaf>oc}Ew08dcFaylex%2@6cI{S^I*@p zX2d?q#2?H6MA!{W1f2n!zwM~PB=tUpk#m}#;&bqMv{_nWIP>NVrR`Al&kbz{hVACD zF7RFkS(H+N7l^hZgcDPkr-erXF63EdVc6B1$Sno5oamDX=jaiu51Ob-p%=Cv|414)mprD4RG_O(itV8 zL|`Ent#*7pLv6{wwo=UP5Zvw_HeXEn5#s=gnbp#Nt|L&E%v&qRyrxw)spw%TMT}z% z=Iwb~LUJN0PI2?t@6Ipwx<(5qkx(Kr&T^PdYAWJE813v?GmxD{Ck(VD&%`~QV9G>? zVT-eF*>cLFZ=9aFsBx9r^2!xB<6oI&0ESndiF&L5)B&q!9)nBeiof8CJ$=u1d{5U> zX7|_3FWVeL3KGT)Fv~04C9!BLH*3=)o(a%rJY94%j3LT)hSad0VY02V)i28TQcJ1mFCK$B-`fmP_TsZLFUKEA z0kqU6oqJu~r<`6cjqn9mlA(uXpZCQ&8Mp$iH44<6SYczV5EUylGXjn|@M;xPiPU?2 z_9oAl+71LyLGQoA>A9yl*nw@ba&BLfv>^vk8)Gfn&>ve3GZ-2aZ?tAGT@aV}^zHc3 z*MFW+S_*AqBq9fFz7xMUst!M71ph|2Y*YZ4#7~ESt4Ai4k2~3X|JYu${+xMLw4L9} zJR9%rzaA~YGqQ-8k@M`{ylg06K&YwEiSa)xqj~xc-*5XF-HP!iDLQ(kN6tfPIoL-? zcj~#dJ$af}<@;=(Iu+aD8E)7KQLx?Hi-{lF4s5KEvEMU27b^tW8QUGejsXdCTashK zz^O@2=_p^L0OY$6*RRzl{Rm|qY4ff#p&5R1-}!AWA8HppaNdV~=bx$kDl&H@&Iw$J zlWs@+zOHwNpjX_5sUQQ2MXV?j=v)b>mI#xKXb+dMmk(GED13KeFYXhSy;^M66qp$VR zRo!3}Lb>Q?sCXIL}R>A#k@ld`soh33^{0`NL5%38~n4-V4L-RZUtz2yHUK&B+XJDXNYDIn}e z;&S(eNkjZaGph5j6uA0H9&ljo^{U{h!ytU=sn--*^8Ka?C$(JRGj5A&wSz^G6GlIoDA5fDade?yRfaNgJ$k797U|v z$4=|Ekpw+!|I{WJtSY%TaF3u?}XeHjom{7z78@a76aQSJc6$fa(ZJ}`d5^`+e(&|Ac7AB2 z@cd(8t?A_D{V8I&cJRz+xbE<~UoMX=-MMgjyj>A!w6t7}Do{81|1G&VVzJisn`5Syu1pa+pQF^dhbu+bvz9*;r z^Li=FaQc;}^zVzsf81fDSBX0czx*6o18`C_{NL{|8RX zWiv{a9-;|YCmT=B0YMA{C0dYgIXKYNN$9(BNpq_SDz=ehI>cTiZRi%(RgxiN7tzFeZ9njx1yNsht(5W2Jw zRF1)b5G>>23nxLsSr(JaB_*;7daak2c3ZWxM4%tA$T%Qt0YQ~(SrXf%o>G0asfhQm zh{0dl>O*cZ(GaIku}9DhF>;XEV;qB)`fb4=-Cjkx;T-bXt;xG|;;OIVNo;!cqV|zzku27}Em|a`j^eHt7zZ z_m?PtsrsHFxF=b)gS){gZwik+5&I~R80f|J`_aPEfTtbjnZ5=`O36PF28LGdl0%!l z(+BB(|Bm5tcUWCA_o|GM4YyBgSt;mP5$=u{XoJ8Rf&=7^#qda?Mh}4%{RA=nih(dP zt8mGsl_JL1{V*#Qlcu->(BR$h?=Z$hDtzw^tZ$}`e>;8+_ALe1T>(GZ%X>oHmKCZ7 zfj55=)Cy*PIDPz<1QJMtLry-~(V`&pYWw2Evk$w3<;NXP)ZG$X(6UgvsER85eL<U4$I@4CAElYWpUiY#ei=XBJT~aG@uD<*(fc51Hh^r(~u?a3t-0 z8O|%MT0I~a{iire8}OZLeJG-@nQee64x z%_Et{%3e@GH9k`W)Mtb_FE6FF*c=9(enZVs2btIL>kEv`Vs}&ot%O;mG>)p&Zj6Y% z1IEm*_Hm-0%)+4l_Ir*pvd-LZV_w7i&9%5-AxfgqSaOZPDo}-9}sr&K})1e%|oTN4-;#GcuJrf&|}-+qJ@9f8-okU$WQanbjn2RMc~0zM%kyDpAC#8 zT(cZISN4He!S(!xGu<+mR4#MozMg%pMJiZEI^PNBS6PHI83$QxVQ31gjbD=@!tI;| zQ`<)l(ZDm7uPbA2JYcqkO#8d8R^M{C-&!@t+sN$TO|oILv@l(z&;G$}!^xgAKGIZD zNM<71m9@b&TASQI8Gy71qnhR#4Kd}Jy3vydkYN^TMlN=P)0Qc{#aqFc@ok^Oj8OKZ z8sYPeJ{l7992VHGBP-lP<`t(UI?%?`??XQKHL_7e&7i)*1dz}i5M&RpT!E>ru-`>b zsk||gr#JQn}|yT6so+NRCT3#djkbYE~)67g{% zaR+cAxz<)q782PfPBTL|ZTvz=M0>D}s~`V}GQfU z!TR*GmpWcyf)Q9DG~8$v2Opd9XL+G9o&hC#8gzf-r8Yy!K~X+!0U?Iz2PM`DyZ+pe z>^Z)?VzK1aUD>~g9lMQ%9Lf>w0g=UBZ^0l)V+?Z-*#>l1x@1C7VgKh_A@wvXa$CC#09qPW! zZmBCX0hnz@uj}$Qw~Nk5S{0kJT@x>ZT??`by(EA1dht5uPhFT9vgy|iKNCIY3%tG( zE5T;iP>&z>IE;O7r~?exHicw$ejIueCX0>lmYLtzm>TToeFoy^ZR{0`7zGFqhJ@}oC z^rnE1zYxsY1{~|-g%`^|3w)!mJO6ZJ$adVU`!lX|Zk{f$q`cG9uu)(8Oqv6&A+4po zj)lVAv2J0%J>8-#n$w z;)T+#=1mo6jTlL|jX1QAo3ucc#42PGXVEL}M=vgPU*_wyf6j&5rKRdH#|%AkN3Nbj zf&`s$HiE*JrxZRB{o~wIrfjxemgZe%fW`;}1rgn%zV@a}Uoen)-T!K~V}3YM*O9=$ z!_W(X$Zr9;_R{i7zW)W4)IsX5Lp1Uve+sAOu(xD8J&b@vn$AOT#}1p|o0(LDcm-RF z(E>$4^o1xE6ZJ~7oyi!7aQ8o}4T-ZC9u&U#68U>hTT~rgBFHP-up?2XM2lwp3rwL? z8u}ui6{D*resw$ADjnX+0(R!u8P$~zP1iIwd`Gp#?KzBp--hHeVfJFh2M>*}8%pT| zacMLQ($P1_b@h-4&H;RHU@ua%CAn}epR2`i9kGy}Qz^?***;54YC2JktN3ikYsNvr zIQ}Zz3hM^E7FG5C$``of9PBy*KKPjHF7_vRG%BbOeJgy7hGxCoCb0QVpDIMN0xuol z>Souwq>x-DOltVUY;tBif7+yb&gywxlBW@n6J((8x8)hdC}kCX;y||@mM(>JXO|g| z=`#tvQ8`wV!M5WA)ny%okJL$N&+PE6GFe;BK4R@;Hsjij`i^kHDXGPphM30(f!B^8 zfnY#Bxg(WYRi*;)%;p+<=Z(Uk#FIRwZ2(T~5_c|JC5bDK9KJYeQqi;*jtzogO@K^H z$ovdOq4R>=`LQL3XT)v5@SC#bGg|c)aTDYXnbqK8QLi{cN1^mp z#X(e&9Va44`vez)hePsxup3*J&aPc$Jupszz5-;IIXP7I_Lm@z7UPo|`xI>4_@L*! z&6M;qVS$~IAQ)FYUc6C&nQx5tb^HBDx39&Jc~G;LoTgkCeDZ2?@nXeikH?uF-NuYD zRQ;eAgL=O&H+!qJ^U$^zvY&^tSn4$~dEkt5SgyCB_{%3-_;`$jhIW0pAXB2)oUJwg zZe-ykbL2uXYQn*>=o`lKFkoq*p3}{Qar-sq$p;uFsO${xAiZj_R}h)M=(c_@C&^Qg z8%}$Im=SqLz7QK+6=Wl;FAf>ddc^-awRw+3QA17#?zC7^-j2$%)%F-y1Dd17<%= zdiFi*S%Wm8qU@P=ca6iJKy0zYzHy**Y+u7BIm8Yqg;VL#TQ}+TacTj)-3RJF71O1U@5Nr`fXJR=e?jhXN0P3FIH~K@lo1AL z??|eV|G-4-UG(tJ{d1|g7^r|d%N zXQXzjzL8q_Rx3N52KSz+_wGeVdnA4*p86JGvea|b-H=U+0MCegjd`&hVE|Aq2ScH| z7PWrpx1h9s7ODNc`ysgAcjf6EGOZ0Pmw9K>^Uk8{ROs*7@Bjgvp7r5@v2)Y0(F^r~ z1=(VHW8}Nk!3*n5@B!X2<#qVQkthQ;xl#fxy;wiUZX7F#eE0E%EI>F3cMJ|*%9!1W zIzDYBdb?XPr^z-7$ps6;y{F!#G0HHF0i@5Sc)fPAe|30dpN3$D*PqJwbsQ0}Y#h^P zOju<~j>y1~_$C7PId`G0?aQyc)X*kL4*bI;qAJOZYZRS>*^Qlx^gVl?ULx>c*R)qu zrZ*Valm76R{A zVU>kC0q{na=xca6T$2JW{9hK7@(arjWMe{Fe zE)2(e6|8?` zSk2lugy?q@^WvZd&I9|&*FqrW*}8GNmL8T4?05Qfpi%F_b<%8Hij?7Y+5!BFn~r+T zTja!AeS&vDH1O&}d$Pk9pMai{?+hX2{?$A(+f#Q14vo>kw;)0FX<$V_N^P%o(4?fP zl$~xBGl}8!goF(oqfXEmPK>ywVUS9u(JG{5@G6g*K$?rSI^vI9oIDyXxfeTWy$kUs z94pNZ;&#e1w3|gcalBcO7du(s-Mri5>`%7)x^3O{s|P0ZS}(Lb=_TiS*N}Lq)D4r;ja75H2yjf?=@8A@p1`)-LW_HFW=g`{-|3*&RlF# zs78Q)_lT5GK2_I`4b~uE?AE2!3x$6sz~LQH8oSj&h*+V1Is&aU>XJsjHr}Z`zWnVo zX|NXfF=*I7-5*XoAb(5BR4*jJ9&K`I?q=nKe7b4fU?wTP&%@3VdoA^4jai0`+qhS+ zpM1`eerA(-k7`bqF?v8TO!Ht3SRH{(#O0`7cyJln* z`kCIeJJ;UnYT1~uZWA@@JJ|EK@gHsbjw|rnHbq46F0|NH3NZc;ouhZ0_I-!1lLdMr ziKOsr*d9Nns*DWvw!ssZJsWNooDM*Z*LF$|34M9P!0zX`W*RTJ3or1K{XyNC5U;H| z`c5YOiHxL|Qw$N-bNpS-63Hs^4=}S$u~wtIu-CTzf=iSA3->(l+^*e5GRI!>4$HA8 zi?mZXlMna7UQ=_a+5W%uoTzG}xH3X#IG{CA>|;;ir)p0*CvhF!4ah~)M|HCBTA zObupmjyzuxOFag`NxZ0cP9p6!dYW9g#t!D$R&H^-$^K zy|Gnj?fRSCRlyX6yWJqwNL+2`^R?jB(9Ht9*taYng*g==Ev^#$yMfk)X!@+MuzQ4% z&-nJtvVqEtjkDCH;JGEJmz~2J<#kQc#`*U9=YiM6b>*5b*A9nabQ`J~&m2>4fHv4P26fQxVku)vZwQ)B*bOJ^F7-qzPUNlxhu24Q(0g3^LoHUZJKvCp>*)q9%#EoF*IV^ zhGVyRhs}dsU+BZ0_i6OH7C49P-{g+eYw;Zf-XUAdRMBMvO;}1dE25b8V6=A zlvA56>Rs=4Q5!M(ZFLd96W0UUwp7l8Y#*bOMr{Tt`~6zutcFb!q;mMo%&Ct4FmzD) zJXS^#P+MxspIfQauQ+f2(&glc+S9W}Q^rN6cT-|6MQeP{)(1+DM7iPNH>AmdlSa*t zxt>)WeekfxNWN?VE=mHmC>b+!vNJY6jB+IPc{RR+n`#iPTJ#O(<&$}g-rTtiTKmW? zT+F0ZPM#|S!@LyaRrUe8lYlwcfF&^X?w;(4Y)L!LI=NUQ?0u{UQ27E{*M{57KaJ(4 zd*@*IC&&44`DADp*~JM3vL4)fXt%n6Srry*%<>U~!W#YU^efNq0T-xbm}KBJEmC*} z){^^bNaXQE&WoVfg8};)VKTs-NS!nA6E z+YG1x57k@{3=^!)Be9w^VdZ_iar8NEw83~=<>Yuxa(g*stOS(Wy=INy?Uzr;0BON8 zCCFYE+EGYoB`~!9uF7f!I3E`YN8U<`_BeeQ(eMEeH)Om^(r+Ca0w3 zr`~+R^igUSjPl<7G99K1BuODYW)hAYxOKnc<)3tEs`~L<BDEzR-bJ^h*Gjm zO365oaD9P`+qsJRBG3lZzslO~zNE$7f8T91?+5iId{zp|ZKDsv{0kZd<_{a^?(_^F z0OAWpm1Y%v3@I{VznINaV85gGQAausx)viR0PA(L?N!*g&+^Gh>8t}=r1@XA^X(Qo z=7C$?qKj6uYMBt``s&#|%~T(MaM^lbZsp)7UBGv`^i@9P9PqrNr+h2M4S2r(`CMy| zZE^Q7WU*+Ow1->`9EpFft2|R*m{-sOxtOwoVU&Nwrd@~kd#&VpL^@4Kz8r%}`sQoz za_9AgZHBEV8Ge7@bdjS%L$t)Gy2XQvxLo)U#Z7>R8R2=e57b%?0jH+qr|&NF30=)} zk6SM3}a`Jrc`a!L0A(v;*Vp0A2GLOidV&igthe4f z{j#b&ncf!g`#t7OO!j4E;HX;lxVn_*-3FNI$v&@&D$g7PK)qHPbH=77(|El04(gY> z{maI?OcrbE1XWU89pZYPkT%08*zutcS|L+)&p>)nD>aS-r{pS76E7~ZGt@E)7bSH1 zDvG`eqOY=|4l1T4$&{314kAiFc!BWp$#9>Mn2X@l>lO{Mhr_1&-$m4V%BMJLOS7!D z?5N-xu{SGQ5Yqmv4F5KO)E~$?T$gzt#iskkW|-r6hr6L^BZV?3N8J%rUoxm|e`wz5 zw1pgd!=-BSGpo!j<2-k_Zg{j)P>=`7L0nB-r$3Y9Emsol80~-!0`cmj{Yhp5H@TG! zrCIo@WUuv#9_L>wZtQKJaORXL%Cg~*aK+zD95u}wl>J%4vfEwdg%^?NpcgoELvt&b z4^K(zC$t?VP5H5Vi^e)$#vS;&f59pT2Ns__;UbsOc)fDTL*`Q2J5J3K6ixP)ZGS(X zB5J!*xSx!&v~+&@2WJt)1oP6w+|^=9(i~d3+h&(_pL}x6v(U1+n3vUA7Rtb|mov%1 zF0KQ5Mo~7eEmnnj>CsR%vA3({<-K}N z)*&o3g&^3qi2~)5lgWzuZ#ru*tm5#Nur1!H??@F z%^xu^$RJA@Q2npbDNftx9lfY!5!!)D$?GS-zn*Q~2Gq64zi2R6SMTg#LD0UvWMX>` zu!V0WBvP3v?~K1iM}`*VZCqD>c#Ty3rdsHbt9_$e1RYAgSttb{9AaTT-fc8glsM%0 zaPp&OZe*`ibjCmZE%ojtRIdE_8MA*YK7X>c%zsn5QrY8w0AVRFD^+{`OI!L=tals^ z#z-05pJc7{$4WiyZ(U3T*&>EAr^>4~kUq6@hV;iI}2yF#cLWrS+D3AZDE|rTO zoihB^x&y$~QlvkX+F@-}$}6j8yjmLnJt|SE3?)5pKpIiFwUp6>g=WKZ;+xRLhlZ_q zHwl^$CE2+mEf&WNajn5VyL~_B7IecWLCug42@AO8`~qd04~vv~v?nbf;%3MKLut`E zGF(c=NE^7TpSwb=T1fOTDB^!q47Sy=9f70YMY9i-yC;@6WPpss;%_^LKu48+NOh(x zsVG&0yz661nQUimprt`ArJtS``6O(a86#W*YGx#aN!X3!5WiAs@uWYVwc4>!Vzu_^}-M{j8xz6+}Rkw`Ge3wu5QO3;@Sf05TfgUy=Ne+bOUXUtb z#40PGH;I8tw}RKa^@U_jNGX**E1Z(K)Rg$f8cse2hEwMMG?7ADN#%_<#q?$D&4=>I z-Q7UD%Q7O2-Stk^%=eVc^sQy?{Vsg_gA5e_-}^r;W8{a?8$|R5@$SN1(&9WtS<}Dr z&Hgw)UGAs{6spSK0l(ULu9^?J`MVL|zfP)h!X#xkTEOr8S%}UrU@H#8=8B?!fog%7D?GHXkL_oi~xi3T&@z$)~6;(da$c z917Id4R(G)v$Ej4z)I#S6Y5t~B2@(}TU{Ni(dPKae>>cG$KdZXYvSpjitV4VejboU z_J7q~P!qG}xWOKgwS@-%w<>bq52RSKazOw4Lw~WiRV)eCNk%^P! Date: Fri, 29 May 2020 10:37:23 +0200 Subject: [PATCH 32/97] fix: merge errors --- .../src/database/Database.test.ts | 8 ++++---- .../src/database/DatabaseManager.test.ts | 4 ++-- plugins/catalog/src/api/types.ts | 6 +++--- plugins/catalog/src/data/utils.ts | 6 +++--- yarn.lock | 17 ++--------------- 5 files changed, 14 insertions(+), 27 deletions(-) diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index bb1a66ac7c..a808e7e567 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -19,16 +19,16 @@ import { getVoidLogger, NotFoundError, } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; import Knex from 'knex'; import path from 'path'; -import { Database } from './Database'; import { - AddDatabaseLocation, DbEntityRequest, DbEntityResponse, + Database, + AddDatabaseLocation, DbLocationsRow, -} from './types'; +} from '.'; +import { Entity } from '@backstage/catalog-model'; describe('Database', () => { let database: Knex; diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts index 9a4297904a..a65bbb7811 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.test.ts @@ -15,12 +15,12 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { Entity, EntityPolicy } from '@backstage/catalog-model'; import Knex from 'knex'; -import { IngestionModel } from '../ingestion/types'; import { Database } from './Database'; import { DatabaseManager } from './DatabaseManager'; import { DatabaseLocationUpdateLogStatus, DbLocationsRow } from './types'; +import { EntityPolicy, Entity } from '@backstage/catalog-model'; +import { IngestionModel } from '..'; describe('DatabaseManager', () => { describe('refreshLocations', () => { diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 6cc1dfd6cc..eb2cdca562 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createApiRef } from '@backstage/core'; -import { DescriptorEnvelope } from '../types'; +import { Entity } from '@backstage/catalog-model'; export const catalogApiRef = createApiRef({ id: 'plugin.catalog.service', @@ -23,6 +23,6 @@ export const catalogApiRef = createApiRef({ }); export interface CatalogApi { - getEntities(): Promise; - getEntityByName(name: string): Promise; + getEntities(): Promise; + getEntityByName(name: string): Promise; } diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts index 39a84cfbca..05fb2fee46 100644 --- a/plugins/catalog/src/data/utils.ts +++ b/plugins/catalog/src/data/utils.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DescriptorEnvelope } from '../api/types'; import { Component } from './component'; +import { Entity } from '@backstage/catalog-model'; -export function envelopeToComponent(envelope: DescriptorEnvelope): Component { +export function envelopeToComponent(envelope: Entity): Component { return { name: envelope.metadata?.name ?? '', kind: envelope.kind ?? 'unknown', - description: envelope.metadata?.description ?? 'placeholder', + description: envelope.metadata?.annotations?.description ?? 'placeholder', }; } diff --git a/yarn.lock b/yarn.lock index 8439f5b0b9..b34d5505d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4290,15 +4290,7 @@ "@types/passport" "*" "@types/passport-oauth2" "*" -"@types/passport-oauth2-refresh@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@types/passport-oauth2-refresh/-/passport-oauth2-refresh-1.1.1.tgz#cbe466d4fcac36182fd75bf55279c0b1e953c382" - integrity sha512-Tw0JvfDPv9asgFPACd9oOGCaD/0/Uyi+QF7fmrJC74cJKC6I8N8wwhJJHyfd1N2E/qaLgTh431lhOa9jicpNdg== - dependencies: - "@types/oauth" "*" - "@types/passport-oauth2" "*" - -"@types/passport-oauth2@*", "@types/passport-oauth2@^1.4.9": +"@types/passport-oauth2@*": version "1.4.9" resolved "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.9.tgz#134007c4b505a82548c9cb19094c5baeb2205c92" integrity sha512-QP0q+NVQOaIu2r0e10QWkiUA0Ya5mOBHRJN0UrI+LolMLOP1/VN4EVIpJ3xVwFo+xqNFRoFvFwJhBvKnk7kpUA== @@ -16110,12 +16102,7 @@ passport-google-oauth20@^2.0.0: dependencies: passport-oauth2 "1.x.x" -passport-oauth2-refresh@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/passport-oauth2-refresh/-/passport-oauth2-refresh-2.0.0.tgz#7b19c77ff3cc000819c69f6ad9e318450f57b85e" - integrity sha512-yXvCB6nem/O+WThhiyI3TlPXpzSGY+9+hy9OTx9QF8e9GInplyRHxHaaOhFylKvnof9UmWHAufQFZk8cO1Fb2g== - -passport-oauth2@1.x.x, passport-oauth2@^1.5.0: +passport-oauth2@1.x.x: version "1.5.0" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== From 7a665c3c2b4b18bb1c4f5ed747058a98a5fd8533 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 29 May 2020 15:16:10 +0200 Subject: [PATCH 33/97] Add github as an auth provider to the backend --- plugins/auth-backend/package.json | 14 ++-- plugins/auth-backend/src/providers/config.ts | 8 ++ .../auth-backend/src/providers/factories.ts | 2 + .../src/providers/github/index.ts | 17 ++++ .../src/providers/github/provider.ts | 84 +++++++++++++++++++ yarn.lock | 33 ++++---- 6 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 plugins/auth-backend/src/providers/github/index.ts create mode 100644 plugins/auth-backend/src/providers/github/provider.ts diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 9cb8017f15..505240853d 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -16,21 +16,23 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.6", + "@types/cookie-parser": "^1.4.2", + "@types/passport": "^1.0.3", + "@types/passport-github2": "^1.2.4", + "@types/passport-google-oauth20": "^2.0.3", "compression": "^1.7.4", + "cookie-parser": "^1.4.5", "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", "helmet": "^3.22.0", "morgan": "^1.10.0", - "winston": "^3.2.1", - "yn": "^4.0.0", "passport": "^0.4.1", + "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", - "cookie-parser": "^1.4.5", - "@types/passport": "^1.0.3", - "@types/passport-google-oauth20": "^2.0.3", - "@types/cookie-parser": "^1.4.2" + "winston": "^3.2.1", + "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", diff --git a/plugins/auth-backend/src/providers/config.ts b/plugins/auth-backend/src/providers/config.ts index 45098afb89..dad87e0448 100644 --- a/plugins/auth-backend/src/providers/config.ts +++ b/plugins/auth-backend/src/providers/config.ts @@ -23,4 +23,12 @@ export const providers = [ callbackURL: 'http://localhost:7000/auth/google/handler/frame', }, }, + { + provider: 'github', + options: { + clientID: process.env.AUTH_GITHUB_CLIENT_ID!, + clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!, + callbackURL: 'http://localhost:7000/auth/github/handler/frame', + }, + }, ]; diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 0a1e639082..10e4153714 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -16,10 +16,12 @@ import { AuthProviderFactories, AuthProviderFactory } from './types'; import { GoogleAuthProvider } from './google'; +import { GithubAuthProvider } from './github'; export class ProviderFactories { private static readonly providerFactories: AuthProviderFactories = { google: GoogleAuthProvider, + github: GithubAuthProvider, }; public static getProviderFactory(providerId: string): AuthProviderFactory { diff --git a/plugins/auth-backend/src/providers/github/index.ts b/plugins/auth-backend/src/providers/github/index.ts new file mode 100644 index 0000000000..c3a48d35e0 --- /dev/null +++ b/plugins/auth-backend/src/providers/github/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { GithubAuthProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts new file mode 100644 index 0000000000..1e8022e1cc --- /dev/null +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { Strategy as GithubStrategy } from 'passport-github2'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, +} from '../PassportStrategyHelper'; +import { + OAuthProviderHandlers, + AuthProviderConfig, + RedirectInfo, + AuthInfoBase, + AuthInfoPrivate, +} from '../types'; + +export class GithubAuthProvider implements OAuthProviderHandlers { + private readonly providerConfig: AuthProviderConfig; + private readonly _strategy: GithubStrategy; + + constructor(providerConfig: AuthProviderConfig) { + this.providerConfig = providerConfig; + this._strategy = new GithubStrategy( + { ...this.providerConfig.options }, + ( + accessToken: any, + refreshToken: any, + params: any, + profile: any, + done: any, + ) => { + done( + undefined, + { + profile, + accessToken, + scope: 'user', // params.scope is an empty string here for some reason, so hardcoding for now + expiresInSeconds: params.expires_in, + }, + { refreshToken }, + ); + }, + ); + } + + async start(req: express.Request, options: any): Promise { + return await executeRedirectStrategy(req, this._strategy, options); + } + + async handler( + req: express.Request, + ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { + return await executeFrameHandlerStrategy(req, this._strategy); + } + + async refresh(refreshToken: string, scope: string): Promise { + const { accessToken, params } = await executeRefreshTokenStrategy( + this._strategy, + refreshToken, + scope, + ); + + return { + accessToken, + expiresInSeconds: params.expires_in, + scope: params.scope, + }; + } +} diff --git a/yarn.lock b/yarn.lock index 8439f5b0b9..30537a6cac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4281,6 +4281,15 @@ resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/passport-github2@^1.2.4": + version "1.2.4" + resolved "https://registry.npmjs.org/@types/passport-github2/-/passport-github2-1.2.4.tgz#f56c386d1fe6435e359430e57adc1747a627bd86" + integrity sha512-dtGtA0Uyzk6ne3SrgQi/I1ClClLE3i7JmSiMaJgkGH8v1nbE9JdBpG7QWJ1XPlLdcf7EvoPdHmkWN2+Kln9y8g== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport-oauth2" "*" + "@types/passport-google-oauth20@^2.0.3": version "2.0.3" resolved "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.3.tgz#f554ff6d39f395acff3f1d762e54462194dac8da" @@ -4290,15 +4299,7 @@ "@types/passport" "*" "@types/passport-oauth2" "*" -"@types/passport-oauth2-refresh@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@types/passport-oauth2-refresh/-/passport-oauth2-refresh-1.1.1.tgz#cbe466d4fcac36182fd75bf55279c0b1e953c382" - integrity sha512-Tw0JvfDPv9asgFPACd9oOGCaD/0/Uyi+QF7fmrJC74cJKC6I8N8wwhJJHyfd1N2E/qaLgTh431lhOa9jicpNdg== - dependencies: - "@types/oauth" "*" - "@types/passport-oauth2" "*" - -"@types/passport-oauth2@*", "@types/passport-oauth2@^1.4.9": +"@types/passport-oauth2@*": version "1.4.9" resolved "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.9.tgz#134007c4b505a82548c9cb19094c5baeb2205c92" integrity sha512-QP0q+NVQOaIu2r0e10QWkiUA0Ya5mOBHRJN0UrI+LolMLOP1/VN4EVIpJ3xVwFo+xqNFRoFvFwJhBvKnk7kpUA== @@ -16103,6 +16104,13 @@ pascalcase@^0.1.1: resolved "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= +passport-github2@^0.1.12: + version "0.1.12" + resolved "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz#a72ebff4fa52a35bc2c71122dcf470d1116f772c" + integrity sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw== + dependencies: + passport-oauth2 "1.x.x" + passport-google-oauth20@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz#0d241b2d21ebd3dc7f2b60669ec4d587e3a674ef" @@ -16110,12 +16118,7 @@ passport-google-oauth20@^2.0.0: dependencies: passport-oauth2 "1.x.x" -passport-oauth2-refresh@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/passport-oauth2-refresh/-/passport-oauth2-refresh-2.0.0.tgz#7b19c77ff3cc000819c69f6ad9e318450f57b85e" - integrity sha512-yXvCB6nem/O+WThhiyI3TlPXpzSGY+9+hy9OTx9QF8e9GInplyRHxHaaOhFylKvnof9UmWHAufQFZk8cO1Fb2g== - -passport-oauth2@1.x.x, passport-oauth2@^1.5.0: +passport-oauth2@1.x.x: version "1.5.0" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== From d3055d432162a17d06bc97a37d6fcab3053e6e67 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 29 May 2020 15:24:38 +0200 Subject: [PATCH 34/97] Add github to auth api --- packages/app/src/apis.ts | 11 ++ .../auth/github/GithubAuth.test.ts | 31 +++++ .../implementations/auth/github/GithubAuth.ts | 118 ++++++++++++++++++ .../apis/implementations/auth/github/index.ts | 18 +++ .../apis/implementations/auth/github/types.ts | 21 ++++ .../src/apis/implementations/auth/index.ts | 1 + 6 files changed, 200 insertions(+) create mode 100644 packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts create mode 100644 packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts create mode 100644 packages/core-api/src/apis/implementations/auth/github/index.ts create mode 100644 packages/core-api/src/apis/implementations/auth/github/types.ts diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 8d76c75d91..d6658c891c 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -25,9 +25,11 @@ import { featureFlagsApiRef, FeatureFlags, GoogleAuth, + GithubAuth, oauthRequestApiRef, OAuthRequestManager, googleAuthApiRef, + githubAuthApiRef, } from '@backstage/core'; import { @@ -63,6 +65,15 @@ builder.add( }), ); +builder.add( + githubAuthApiRef, + GithubAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), +); + builder.add( techRadarApiRef, new TechRadar({ diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts new file mode 100644 index 0000000000..3d3da266fc --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import GithubAuth from './GithubAuth'; + +const theFuture = new Date(Date.now() + 3600000); + +describe('GithubAuth', () => { + it('should get refreshed access token', async () => { + const getSession = jest + .fn() + .mockResolvedValue({ accessToken: 'access-token', expiresAt: theFuture }); + const githubAuth = new GithubAuth({ getSession } as any); + + expect(await githubAuth.getAccessToken()).toBe('access-token'); + expect(getSession).toBeCalledTimes(1); + }); +}); diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts new file mode 100644 index 0000000000..d20eaa54cb --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -0,0 +1,118 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import GithubIcon from '@material-ui/icons/AcUnit'; +import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; +import { GithubSession } from './types'; +import { OAuthApi, AccessTokenOptions } from '../../../definitions/auth'; +import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; + +type CreateOptions = { + // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth + apiOrigin: string; + basePath: string; + + oauthRequestApi: OAuthRequestApi; + + environment?: string; + provider?: AuthProvider & { id: string }; +}; + +export type GithubAuthResponse = { + accessToken: string; + idToken: string; + scope: string; + expiresInSeconds: number; +}; + +const DEFAULT_PROVIDER = { + id: 'github', + title: 'Github', + icon: GithubIcon, +}; + +class GithubAuth implements OAuthApi { + static create({ + apiOrigin, + basePath, + environment = 'dev', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + }: CreateOptions) { + const connector = new DefaultAuthConnector({ + apiOrigin, + basePath, + environment, + provider, + oauthRequestApi: oauthRequestApi, + sessionTransform(res: GithubAuthResponse): GithubSession { + return { + accessToken: res.accessToken, + scopes: GithubAuth.normalizeScopes(res.scope), + expiresAt: new Date(Date.now() + res.expiresInSeconds * 1000), + }; + }, + }); + + const sessionManager = new RefreshingAuthSessionManager({ + connector, + defaultScopes: new Set(['user']), + sessionScopes: session => session.scopes, + sessionShouldRefresh: session => { + const expiresInSec = (session.expiresAt.getTime() - Date.now()) / 1000; + return expiresInSec < 60 * 5; + }, + }); + + return new GithubAuth(sessionManager); + } + + constructor(private readonly sessionManager: SessionManager) {} + + async getAccessToken( + scope?: string | string[], + options?: AccessTokenOptions, + ) { + const normalizedScopes = GithubAuth.normalizeScopes(scope); + const session = await this.sessionManager.getSession({ + ...options, + scopes: normalizedScopes, + }); + if (session) { + return session.accessToken; + } + return ''; + } + + async logout() { + await this.sessionManager.removeSession(); + } + + static normalizeScopes(scopes?: string | string[]): Set { + if (!scopes) { + return new Set(); + } + + const scopeList = Array.isArray(scopes) + ? scopes + : scopes.split(/[\s]/).filter(Boolean); + + return new Set(scopeList); + } +} +export default GithubAuth; diff --git a/packages/core-api/src/apis/implementations/auth/github/index.ts b/packages/core-api/src/apis/implementations/auth/github/index.ts new file mode 100644 index 0000000000..9e1722f4a4 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/github/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export * from './types'; +export { default as GithubAuth } from './GithubAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/github/types.ts b/packages/core-api/src/apis/implementations/auth/github/types.ts new file mode 100644 index 0000000000..282017b80d --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/github/types.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export type GithubSession = { + accessToken: string; + scopes: Set; + expiresAt: Date; +}; diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts index 5fa6644b2a..f13368b5c4 100644 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ b/packages/core-api/src/apis/implementations/auth/index.ts @@ -15,3 +15,4 @@ */ export * from './google'; +export * from './github'; From ce22236c3cba8f8add0224196c2aaf8b7914fe86 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 May 2020 18:14:50 +0200 Subject: [PATCH 35/97] packages/core-api: added initial ConfigApi --- .../src/apis/definitions/ConfigApi.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/core-api/src/apis/definitions/ConfigApi.ts diff --git a/packages/core-api/src/apis/definitions/ConfigApi.ts b/packages/core-api/src/apis/definitions/ConfigApi.ts new file mode 100644 index 0000000000..20676df899 --- /dev/null +++ b/packages/core-api/src/apis/definitions/ConfigApi.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createApiRef } from '../ApiRef'; + +export type Config = { + getConfig(key: string): Config; + + getConfigArray(key: string): Config[]; + + getNumber(key: string): number | undefined; + + getBoolean(key: string): boolean | undefined; + + getString(key: string): string | undefined; + + getStringArray(key: string): string[] | undefined; +}; + +// Using interface to make the ConfigApi name show up in docs +export interface ConfigApi extends Config {} + +export const configApiRef = createApiRef({ + id: 'core.config', + description: 'Used to access runtime configuration', +}); From 0c6dbe0545360cce2ef41f0e8c600c2cc16759db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 May 2020 20:46:31 +0200 Subject: [PATCH 36/97] packages/core-api: added initial ConfigReader --- .../ConfigApi/ConfigReader.test.ts | 126 ++++++++++++++ .../implementations/ConfigApi/ConfigReader.ts | 164 ++++++++++++++++++ .../apis/implementations/ConfigApi/index.ts | 17 ++ 3 files changed, 307 insertions(+) create mode 100644 packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts create mode 100644 packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts create mode 100644 packages/core-api/src/apis/implementations/ConfigApi/index.ts diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts new file mode 100644 index 0000000000..206527b95d --- /dev/null +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts @@ -0,0 +1,126 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from './ConfigReader'; + +const DATA = { + zero: 0, + one: 1, + true: true, + false: false, + null: null, + string: 'string', + emptyString: '', + strings: ['string1', 'string2'], + badStrings: ['string1', ''], + worseStrings: ['string1', 3] as string[], + worstStrings: ['string1', 'string2', {}] as string[], + nested: { + one: 1, + string: 'string', + strings: ['string1', 'string2'], + }, + nestlings: [{ boolean: true }, { string: 'string' }, { number: 42 }] as {}[], +}; + +describe('ConfigReader', () => { + it('should read empty config with valid keys', () => { + const config = new ConfigReader({}); + expect(config.getString('x')).toBeUndefined(); + expect(config.getString('x_x')).toBeUndefined(); + expect(config.getString('x-X')).toBeUndefined(); + expect(config.getString('x0')).toBeUndefined(); + expect(config.getString('X-x2')).toBeUndefined(); + expect(config.getString('x0_x0')).toBeUndefined(); + expect(config.getString('x_x-x_x')).toBeUndefined(); + }); + + it('should throw on invalid keys', () => { + const config = new ConfigReader({}); + + expect(() => config.getString('.')).toThrow(/^Invalid config key/); + expect(() => config.getString('0')).toThrow(/^Invalid config key/); + expect(() => config.getString('(')).toThrow(/^Invalid config key/); + expect(() => config.getString('z-_')).toThrow(/^Invalid config key/); + expect(() => config.getString('-')).toThrow(/^Invalid config key/); + expect(() => config.getString('.a')).toThrow(/^Invalid config key/); + expect(() => config.getString('0.a')).toThrow(/^Invalid config key/); + expect(() => config.getString('0a')).toThrow(/^Invalid config key/); + expect(() => config.getString('a.0a')).toThrow(/^Invalid config key/); + expect(() => config.getString('a..a')).toThrow(/^Invalid config key/); + expect(() => config.getString('a.')).toThrow(/^Invalid config key/); + expect(() => config.getString('a...')).toThrow(/^Invalid config key/); + expect(() => config.getString('a.a.a.a.')).toThrow(/^Invalid config key/); + expect(() => config.getString('a._')).toThrow(/^Invalid config key/); + expect(() => config.getString('a.-.a')).toThrow(/^Invalid config key/); + }); + + it('should read valid values', () => { + const config = new ConfigReader(DATA); + expect(config.getNumber('zero')).toBe(0); + expect(config.getNumber('one')).toBe(1); + expect(config.getBoolean('true')).toBe(true); + expect(config.getBoolean('false')).toBe(false); + expect(config.getString('string')).toBe('string'); + expect(config.getStringArray('strings')).toEqual(['string1', 'string2']); + expect(config.getConfig('nested').getNumber('one')).toBe(1); + expect(config.getConfig('nested').getString('string')).toBe('string'); + expect(config.getConfig('nested').getStringArray('strings')).toEqual([ + 'string1', + 'string2', + ]); + + const [config1, config2, config3] = config.getConfigArray('nestlings'); + expect(config1.getBoolean('boolean')).toBe(true); + expect(config2.getString('string')).toBe('string'); + expect(config3.getNumber('number')).toBe(42); + }); + + it('should fail to read invalid values', () => { + const config = new ConfigReader(DATA); + + expect(() => config.getNumber('string')).toThrow( + 'Invalid type in config for key string, got string, wanted number', + ); + expect(() => config.getString('one')).toThrow( + 'Invalid type in config for key one, got number, wanted string', + ); + expect(() => config.getNumber('true')).toThrow( + 'Invalid type in config for key true, got boolean, wanted number', + ); + expect(() => config.getStringArray('null')).toThrow( + 'Invalid type in config for key null, got null, wanted string-array', + ); + expect(() => config.getString('emptyString')).toThrow( + 'Invalid type in config for key emptyString, got empty-string, wanted string', + ); + expect(() => config.getStringArray('badStrings')).toThrow( + 'Invalid type in config for key badStrings[1], got empty-string, wanted string', + ); + expect(() => config.getStringArray('worseStrings')).toThrow( + 'Invalid type in config for key worseStrings[1], got number, wanted string', + ); + expect(() => config.getStringArray('worstStrings')).toThrow( + 'Invalid type in config for key worstStrings[2], got object, wanted string', + ); + expect(() => config.getConfig('one')).toThrow( + 'Invalid type in config for key one, got number, wanted object', + ); + expect(() => config.getConfigArray('one')).toThrow( + 'Invalid type in config for key one, got number, wanted object-array', + ); + }); +}); diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts new file mode 100644 index 0000000000..ac725ba59d --- /dev/null +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts @@ -0,0 +1,164 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigApi, Config } from '../../definitions/ConfigApi'; + +const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; + +type JsonObject = { [key in string]: JsonValue }; +type JsonArray = JsonValue[]; +type JsonValue = JsonObject | JsonArray | number | string | boolean | null; + +function isObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function typeOf(value: JsonValue | undefined): string { + if (value === null) { + return 'null'; + } else if (Array.isArray(value)) { + return 'array'; + } + const type = typeof value; + if (type === 'number' && isNaN(value as number)) { + return 'nan'; + } + return type; +} + +function typeErrorMessage(key: string, got: string, wanted: string) { + return `Invalid type in config for key ${key}, got ${got}, wanted ${wanted}`; +} + +function validateString( + key: string, + value: JsonValue | undefined, +): value is string { + if (typeof value === 'string' && value.length > 0) { + return true; + } + if (value === '') { + throw new TypeError(typeErrorMessage(key, 'empty-string', 'string')); + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'string')); + } + return false; +} + +export class ConfigReader implements ConfigApi { + static nullReader = new ConfigReader({}); + + constructor(private readonly data: JsonObject) {} + + getConfig(key: string): Config { + const value = this.readValue(key); + if (isObject(value)) { + return new ConfigReader(value); + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'object')); + } + return ConfigReader.nullReader; + } + + getConfigArray(key: string): Config[] { + const values = this.readValue(key); + if (Array.isArray(values)) { + return values.map((value, index) => { + if (isObject(value)) { + return new ConfigReader(value); + } + throw new TypeError( + typeErrorMessage(`${key}[${index}]`, typeOf(value), 'object'), + ); + }); + } + if (values !== undefined) { + throw new TypeError( + typeErrorMessage(key, typeOf(values), 'object-array'), + ); + } + return []; + } + + getNumber(key: string): number | undefined { + const value = this.readValue(key); + if (typeof value === 'number' && !isNaN(value)) { + return value; + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'number')); + } + return undefined; + } + + getBoolean(key: string): boolean | undefined { + const value = this.readValue(key); + if (typeof value === 'boolean') { + return value; + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'boolean')); + } + return undefined; + } + + getString(key: string): string | undefined { + const value = this.readValue(key); + if (validateString(key, value)) { + return value; + } + return undefined; + } + + getStringArray(key: string): string[] | undefined { + const values = this.readValue(key); + if (Array.isArray(values)) { + for (const [index, value] of values.entries()) { + const iKey = `${key}[${index}]`; + if (!validateString(iKey, value)) { + throw new TypeError(typeErrorMessage(iKey, typeOf(value), 'string')); + } + } + return values as string[]; + } + if (values !== undefined) { + throw new TypeError( + typeErrorMessage(key, typeOf(values), 'string-array'), + ); + } + return undefined; + } + + private readValue(key: string): JsonValue | undefined { + const parts = key.split('.'); + + let value: JsonValue | undefined = this.data; + for (const part of parts) { + if (!CONFIG_KEY_PART_PATTERN.test(part)) { + throw new TypeError(`Invalid config key '${key}'`); + } + if (isObject(value)) { + value = value[part]; + } else { + value = undefined; + } + } + + return value; + } +} diff --git a/packages/core-api/src/apis/implementations/ConfigApi/index.ts b/packages/core-api/src/apis/implementations/ConfigApi/index.ts new file mode 100644 index 0000000000..8839cb948e --- /dev/null +++ b/packages/core-api/src/apis/implementations/ConfigApi/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { ConfigReader } from './ConfigReader'; From d471182708b55fd617008416da6908c724e33687 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 12:57:33 +0200 Subject: [PATCH 37/97] packages/core-api: add fallback capability to ConfigReader --- .../ConfigApi/ConfigReader.test.ts | 133 +++++++++++------- .../implementations/ConfigApi/ConfigReader.ts | 20 +-- 2 files changed, 97 insertions(+), 56 deletions(-) diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts index 206527b95d..a9731c0d2c 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts @@ -36,6 +36,59 @@ const DATA = { nestlings: [{ boolean: true }, { string: 'string' }, { number: 42 }] as {}[], }; +function expectValidValues(config: ConfigReader) { + expect(config.getNumber('zero')).toBe(0); + expect(config.getNumber('one')).toBe(1); + expect(config.getBoolean('true')).toBe(true); + expect(config.getBoolean('false')).toBe(false); + expect(config.getString('string')).toBe('string'); + expect(config.getStringArray('strings')).toEqual(['string1', 'string2']); + expect(config.getConfig('nested').getNumber('one')).toBe(1); + expect(config.getConfig('nested').getString('string')).toBe('string'); + expect(config.getConfig('nested').getStringArray('strings')).toEqual([ + 'string1', + 'string2', + ]); + + const [config1, config2, config3] = config.getConfigArray('nestlings'); + expect(config1.getBoolean('boolean')).toBe(true); + expect(config2.getString('string')).toBe('string'); + expect(config3.getNumber('number')).toBe(42); +} + +function expectInvalidValues(config: ConfigReader) { + expect(() => config.getNumber('string')).toThrow( + 'Invalid type in config for key string, got string, wanted number', + ); + expect(() => config.getString('one')).toThrow( + 'Invalid type in config for key one, got number, wanted string', + ); + expect(() => config.getNumber('true')).toThrow( + 'Invalid type in config for key true, got boolean, wanted number', + ); + expect(() => config.getStringArray('null')).toThrow( + 'Invalid type in config for key null, got null, wanted string-array', + ); + expect(() => config.getString('emptyString')).toThrow( + 'Invalid type in config for key emptyString, got empty-string, wanted string', + ); + expect(() => config.getStringArray('badStrings')).toThrow( + 'Invalid type in config for key badStrings[1], got empty-string, wanted string', + ); + expect(() => config.getStringArray('worseStrings')).toThrow( + 'Invalid type in config for key worseStrings[1], got number, wanted string', + ); + expect(() => config.getStringArray('worstStrings')).toThrow( + 'Invalid type in config for key worstStrings[2], got object, wanted string', + ); + expect(() => config.getConfig('one')).toThrow( + 'Invalid type in config for key one, got number, wanted object', + ); + expect(() => config.getConfigArray('one')).toThrow( + 'Invalid type in config for key one, got number, wanted object-array', + ); +} + describe('ConfigReader', () => { it('should read empty config with valid keys', () => { const config = new ConfigReader({}); @@ -70,57 +123,41 @@ describe('ConfigReader', () => { it('should read valid values', () => { const config = new ConfigReader(DATA); - expect(config.getNumber('zero')).toBe(0); - expect(config.getNumber('one')).toBe(1); - expect(config.getBoolean('true')).toBe(true); - expect(config.getBoolean('false')).toBe(false); - expect(config.getString('string')).toBe('string'); - expect(config.getStringArray('strings')).toEqual(['string1', 'string2']); - expect(config.getConfig('nested').getNumber('one')).toBe(1); - expect(config.getConfig('nested').getString('string')).toBe('string'); - expect(config.getConfig('nested').getStringArray('strings')).toEqual([ - 'string1', - 'string2', - ]); - - const [config1, config2, config3] = config.getConfigArray('nestlings'); - expect(config1.getBoolean('boolean')).toBe(true); - expect(config2.getString('string')).toBe('string'); - expect(config3.getNumber('number')).toBe(42); + expectValidValues(config); }); it('should fail to read invalid values', () => { const config = new ConfigReader(DATA); - - expect(() => config.getNumber('string')).toThrow( - 'Invalid type in config for key string, got string, wanted number', - ); - expect(() => config.getString('one')).toThrow( - 'Invalid type in config for key one, got number, wanted string', - ); - expect(() => config.getNumber('true')).toThrow( - 'Invalid type in config for key true, got boolean, wanted number', - ); - expect(() => config.getStringArray('null')).toThrow( - 'Invalid type in config for key null, got null, wanted string-array', - ); - expect(() => config.getString('emptyString')).toThrow( - 'Invalid type in config for key emptyString, got empty-string, wanted string', - ); - expect(() => config.getStringArray('badStrings')).toThrow( - 'Invalid type in config for key badStrings[1], got empty-string, wanted string', - ); - expect(() => config.getStringArray('worseStrings')).toThrow( - 'Invalid type in config for key worseStrings[1], got number, wanted string', - ); - expect(() => config.getStringArray('worstStrings')).toThrow( - 'Invalid type in config for key worstStrings[2], got object, wanted string', - ); - expect(() => config.getConfig('one')).toThrow( - 'Invalid type in config for key one, got number, wanted object', - ); - expect(() => config.getConfigArray('one')).toThrow( - 'Invalid type in config for key one, got number, wanted object-array', - ); + expectInvalidValues(config); + }); +}); + +describe('ConfigReader with fallback', () => { + it('should behave as if without fallback', () => { + const config = new ConfigReader({}, new ConfigReader(DATA)); + expect(config.getString('x')).toBeUndefined(); + expect(() => config.getString('.')).toThrow(/^Invalid config key/); + expect(() => config.getString('a.')).toThrow(/^Invalid config key/); + }); + + it('should read values from itself', () => { + const config = new ConfigReader(DATA, new ConfigReader({})); + expectValidValues(config); + expectInvalidValues(config); + }); + + it('should read values from a fallback', () => { + const config = new ConfigReader({}, new ConfigReader(DATA)); + expectValidValues(config); + expectInvalidValues(config); + }); + + it('should read values from multiple levels of fallbacks', () => { + const config = new ConfigReader( + {}, + new ConfigReader({}, new ConfigReader({}, new ConfigReader(DATA))), + ); + expectValidValues(config); + expectInvalidValues(config); }); }); diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts index ac725ba59d..7a5cf3b185 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts @@ -62,17 +62,21 @@ function validateString( export class ConfigReader implements ConfigApi { static nullReader = new ConfigReader({}); - constructor(private readonly data: JsonObject) {} + constructor( + private readonly data: JsonObject, + private readonly fallback?: ConfigApi, + ) {} getConfig(key: string): Config { const value = this.readValue(key); + const fallbackConfig = this.fallback?.getConfig(key); if (isObject(value)) { - return new ConfigReader(value); + return new ConfigReader(value, fallbackConfig); } if (value !== undefined) { throw new TypeError(typeErrorMessage(key, typeOf(value), 'object')); } - return ConfigReader.nullReader; + return fallbackConfig ?? ConfigReader.nullReader; } getConfigArray(key: string): Config[] { @@ -92,7 +96,7 @@ export class ConfigReader implements ConfigApi { typeErrorMessage(key, typeOf(values), 'object-array'), ); } - return []; + return this.fallback?.getConfigArray(key) ?? []; } getNumber(key: string): number | undefined { @@ -103,7 +107,7 @@ export class ConfigReader implements ConfigApi { if (value !== undefined) { throw new TypeError(typeErrorMessage(key, typeOf(value), 'number')); } - return undefined; + return this.fallback?.getNumber(key); } getBoolean(key: string): boolean | undefined { @@ -114,7 +118,7 @@ export class ConfigReader implements ConfigApi { if (value !== undefined) { throw new TypeError(typeErrorMessage(key, typeOf(value), 'boolean')); } - return undefined; + return this.fallback?.getBoolean(key); } getString(key: string): string | undefined { @@ -122,7 +126,7 @@ export class ConfigReader implements ConfigApi { if (validateString(key, value)) { return value; } - return undefined; + return this.fallback?.getString(key); } getStringArray(key: string): string[] | undefined { @@ -141,7 +145,7 @@ export class ConfigReader implements ConfigApi { typeErrorMessage(key, typeOf(values), 'string-array'), ); } - return undefined; + return this.fallback?.getStringArray(key); } private readValue(key: string): JsonValue | undefined { From 810de67f9c7a378d587c303bc271746a6bdab7f0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 13:16:59 +0200 Subject: [PATCH 38/97] packages/core-api: some more tests for ConfigReader to make sure deep merge is correct --- .../ConfigApi/ConfigReader.test.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts index a9731c0d2c..68c1fd5353 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts @@ -160,4 +160,67 @@ describe('ConfigReader with fallback', () => { expectValidValues(config); expectInvalidValues(config); }); + + it('should read merged objects', () => { + const a = { + merged: { + x: 'x', + z: 'z1', + arr: ['a', 'b'], + config: { d: 'd' }, + configs: [{ a: 'a' }], + }, + }; + const b = { + merged: { + y: 'y', + z: 'z2', + arr: ['c'], + config: { e: 'e' }, + configs: [{ b: 'b' }], + }, + }; + + const config = new ConfigReader(a, new ConfigReader(b)); + + expect(config.getString('merged.x')).toBe('x'); + expect(config.getString('merged.y')).toBe('y'); + expect(config.getString('merged.z')).toBe('z1'); + expect(config.getConfig('merged').getString('x')).toBe('x'); + expect(config.getConfig('merged').getString('y')).toBe('y'); + expect(config.getConfig('merged').getString('z')).toBe('z1'); + expect(config.getString('merged.config.d')).toBe('d'); + expect(config.getString('merged.config.e')).toBe('e'); + expect(config.getConfig('merged').getString('config.d')).toBe('d'); + expect(config.getConfig('merged').getString('config.e')).toBe('e'); + expect(config.getConfig('merged').getConfig('config').getString('d')).toBe( + 'd', + ); + expect(config.getConfig('merged').getConfig('config').getString('e')).toBe( + 'e', + ); + + // Arrays are not merged + expect(config.getStringArray('merged.arr')).toEqual(['a', 'b']); + expect(config.getConfig('merged').getStringArray('arr')).toEqual([ + 'a', + 'b', + ]); + + // Config arrays aren't merged either + expect(config.getConfigArray('merged.configs').length).toBe(1); + expect(config.getConfigArray('merged.configs')[0].getString('a')).toBe('a'); + expect( + config.getConfigArray('merged.configs')[0].getString('b'), + ).toBeUndefined(); + + // Config arrays aren't merged either + expect(config.getConfig('merged').getConfigArray('configs').length).toBe(1); + expect( + config.getConfig('merged').getConfigArray('configs')[0].getString('a'), + ).toBe('a'); + expect( + config.getConfig('merged').getConfigArray('configs')[0].getString('b'), + ).toBeUndefined(); + }); }); From 1ab6499bbbe1abede1b0d3ab1751557f72a6228c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 13:53:06 +0200 Subject: [PATCH 39/97] packages/core-api: export ConfigApi + implementation --- packages/core-api/src/apis/definitions/index.ts | 1 + packages/core-api/src/apis/implementations/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/core-api/src/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts index 2e008965cd..2e9db325dc 100644 --- a/packages/core-api/src/apis/definitions/index.ts +++ b/packages/core-api/src/apis/definitions/index.ts @@ -24,6 +24,7 @@ export * from './auth'; export * from './AlertApi'; export * from './AppThemeApi'; +export * from './ConfigApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; export * from './OAuthRequestApi'; diff --git a/packages/core-api/src/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts index bb77cf5bd3..b5cc250ae4 100644 --- a/packages/core-api/src/apis/implementations/index.ts +++ b/packages/core-api/src/apis/implementations/index.ts @@ -22,5 +22,6 @@ export * from './auth'; export * from './AlertApi'; export * from './AppThemeApi'; +export * from './ConfigApi'; export * from './ErrorApi'; export * from './OAuthRequestApi'; From bdf32a00a031bc6cf67bb73940138871e16f1abe Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 14:16:43 +0200 Subject: [PATCH 40/97] packages/core-api: add configLoader option to App --- packages/core-api/src/app/App.tsx | 44 ++++++++++++++------ packages/core-api/src/app/types.ts | 27 ++++++++++++ packages/core/src/api-wrappers/createApp.tsx | 27 +++++++++++- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 844c38ecce..c3ea77d87f 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -17,7 +17,7 @@ import React, { ComponentType, FC } from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; import { AppContextProvider } from './AppContext'; -import { BackstageApp, AppComponents } from './types'; +import { BackstageApp, AppComponents, AppConfigLoader } from './types'; import { BackstagePlugin } from '../plugin'; import { FeatureFlagsRegistryItem } from './FeatureFlags'; import { featureFlagsApiRef } from '../apis/definitions'; @@ -31,8 +31,11 @@ import { AppTheme, AppThemeSelector, appThemeApiRef, + configApiRef, + ConfigReader, } from '../apis'; import { ApiAggregator } from '../apis/ApiAggregator'; +import { useAsync } from 'react-use'; type FullAppOptions = { apis: ApiHolder; @@ -40,6 +43,7 @@ type FullAppOptions = { plugins: BackstagePlugin[]; components: AppComponents; themes: AppTheme[]; + configLoader: AppConfigLoader; }; export class PrivateAppImpl implements BackstageApp { @@ -48,6 +52,7 @@ export class PrivateAppImpl implements BackstageApp { private readonly plugins: BackstagePlugin[]; private readonly components: AppComponents; private readonly themes: AppTheme[]; + private readonly configLoader: AppConfigLoader; constructor(options: FullAppOptions) { this.apis = options.apis; @@ -55,6 +60,7 @@ export class PrivateAppImpl implements BackstageApp { this.plugins = options.plugins; this.components = options.components; this.themes = options.themes; + this.configLoader = options.configLoader; } getApis(): ApiHolder { @@ -141,18 +147,32 @@ export class PrivateAppImpl implements BackstageApp { } getProvider(): ComponentType<{}> { - const appApis = ApiRegistry.from([ - [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)], - ]); - const apis = new ApiAggregator(this.apis, appApis); + const Provider: FC<{}> = ({ children }) => { + const config = useAsync(this.configLoader); + if (config.loading) { + return null; + } - const Provider: FC<{}> = ({ children }) => ( - - - {children} - - - ); + let errorPage = undefined; + if (config.error) { + const { BootErrorPage } = this.components; + errorPage = ; + } + + const appApis = ApiRegistry.from([ + [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)], + [configApiRef, new ConfigReader(config.value ?? {})], + ]); + const apis = new ApiAggregator(this.apis, appApis); + + return ( + + + {errorPage ?? children} + + + ); + }; return Provider; } diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index ea3a812557..e43a625569 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -20,10 +20,26 @@ import { BackstagePlugin } from '../plugin'; import { ApiHolder } from '../apis'; import { AppTheme } from '../apis/definitions'; +export type BootErrorPageProps = { + step: 'load-config'; + error: Error; +}; + export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; + BootErrorPage: ComponentType; }; +/** + * TBD + */ +export type AppConfig = any; + +/** + * A function that loads in the App config that will be accessible via the ConfigApi. + */ +export type AppConfigLoader = () => Promise; + export type AppOptions = { /** * A holder of all APIs available in the app. @@ -68,6 +84,17 @@ export type AppOptions = { * ``` */ themes?: AppTheme[]; + + /** + * A function that loads in App configuration that will be accessible via + * the ConfigApi. + * + * Defaults to an empty config. + * + * TODO(Rugvip): Omitting this should instead default to loading in configuration + * that was packaged by the backstage-cli and default docker container boot script. + */ + configLoader?: AppConfigLoader; }; export type BackstageApp = { diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index 03adc41ec6..cc083b0e24 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -14,12 +14,14 @@ * limitations under the License. */ -import React from 'react'; +import React, { FC } from 'react'; import privateExports, { AppOptions, ApiRegistry, defaultSystemIcons, + BootErrorPageProps, } from '@backstage/core-api'; +import { BrowserRouter as Router } from 'react-router-dom'; import { ErrorPage } from '../layout/ErrorPage'; import { lightTheme, darkTheme } from '@backstage/theme'; @@ -38,12 +40,25 @@ export function createApp(options?: AppOptions) { const DefaultNotFoundPage = () => ( ); + const DefaultBootErrorPage: FC = ({ step, error }) => { + let message = ''; + if (step === 'load-config') { + message = `The configuration failed to load, someone should have a look at this error: ${error.message}`; + } + // TODO: figure out a nicer way to handle routing on the error page, when it can be done. + return ( + + + + ); + }; const apis = options?.apis ?? ApiRegistry.from([]); const icons = { ...defaultSystemIcons, ...options?.icons }; const plugins = options?.plugins ?? []; const components = { NotFoundErrorPage: DefaultNotFoundPage, + BootErrorPage: DefaultBootErrorPage, ...options?.components, }; const themes = options?.themes ?? [ @@ -60,8 +75,16 @@ export function createApp(options?: AppOptions) { theme: darkTheme, }, ]; + const configLoader = options?.configLoader ?? (async () => ({})); - const app = new PrivateAppImpl({ apis, icons, plugins, components, themes }); + const app = new PrivateAppImpl({ + apis, + icons, + plugins, + components, + themes, + configLoader, + }); app.verify(); From a115bd63311f8134be0d173d8918af22052d1068 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 14:26:21 +0200 Subject: [PATCH 41/97] packages/app: add mock config loader and use in welcome plugin --- packages/app/src/App.tsx | 12 ++++++++++++ .../src/components/WelcomePage/WelcomePage.test.tsx | 13 +++++++++++-- .../src/components/WelcomePage/WelcomePage.tsx | 5 ++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 56faf4402b..461d8f193b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -29,6 +29,18 @@ import apis from './apis'; const app = createApp({ apis, plugins: Object.values(plugins), + configLoader: async () => ({ + app: { + title: 'Backstage Example App', + baseUrl: 'http://localhost:3000', + }, + backend: { + baseUrl: 'http://localhost:7000', + }, + organization: { + name: 'Spotify', + }, + }), }); const AppProvider = app.getProvider(); diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx index 9cba76fddc..b639da9d50 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx @@ -19,14 +19,23 @@ import { render } from '@testing-library/react'; import WelcomePage from './WelcomePage'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { + ApiProvider, + ApiRegistry, + errorApiRef, + configApiRef, + ConfigReader, +} from '@backstage/core'; describe('WelcomePage', () => { it('should render', () => { // TODO: use common test app with mock implementations of all core APIs const rendered = render( diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx index 1036096ae5..6a1b0e0008 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx @@ -34,15 +34,18 @@ import { ContentHeader, SupportButton, WarningPanel, + useApi, + configApiRef, } from '@backstage/core'; const WelcomePage: FC<{}> = () => { + const appTitle = useApi(configApiRef).getString('app.title') ?? 'Backstage'; const profile = { givenName: '' }; return (

From d120e5f8cb9116aa89eb1c479939ed410ce29c29 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 19:55:02 +0200 Subject: [PATCH 42/97] packages/core-api: add Progress as a configurable app component --- packages/core-api/src/app/App.tsx | 15 ++++++++------- packages/core-api/src/app/types.ts | 1 + packages/core/src/api-wrappers/createApp.tsx | 2 ++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index c3ea77d87f..49d2291acd 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -149,14 +149,15 @@ export class PrivateAppImpl implements BackstageApp { getProvider(): ComponentType<{}> { const Provider: FC<{}> = ({ children }) => { const config = useAsync(this.configLoader); - if (config.loading) { - return null; - } - let errorPage = undefined; - if (config.error) { + let childNode = children; + + if (config.loading) { + const { Progress } = this.components; + childNode = ; + } else if (config.error) { const { BootErrorPage } = this.components; - errorPage = ; + childNode = ; } const appApis = ApiRegistry.from([ @@ -168,7 +169,7 @@ export class PrivateAppImpl implements BackstageApp { return ( - {errorPage ?? children} + {childNode} ); diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index e43a625569..defc155a82 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -28,6 +28,7 @@ export type BootErrorPageProps = { export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; BootErrorPage: ComponentType; + Progress: ComponentType<{}>; }; /** diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index cc083b0e24..7c605c4e18 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -24,6 +24,7 @@ import privateExports, { import { BrowserRouter as Router } from 'react-router-dom'; import { ErrorPage } from '../layout/ErrorPage'; +import Progress from '../components/Progress'; import { lightTheme, darkTheme } from '@backstage/theme'; const { PrivateAppImpl } = privateExports; @@ -59,6 +60,7 @@ export function createApp(options?: AppOptions) { const components = { NotFoundErrorPage: DefaultNotFoundPage, BootErrorPage: DefaultBootErrorPage, + Progress: Progress, ...options?.components, }; const themes = options?.themes ?? [ From 8f56253f2a250ae25cf6a47c8fb3f186af30c6b8 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Sat, 30 May 2020 11:36:57 +0200 Subject: [PATCH 43/97] remove github login page --- packages/app/src/App.tsx | 10 +- .../core/src/layout/LoginPage/LoginPage.tsx | 178 ------------------ packages/core/src/layout/LoginPage/index.ts | 17 -- packages/core/src/layout/index.ts | 1 - 4 files changed, 2 insertions(+), 204 deletions(-) delete mode 100644 packages/core/src/layout/LoginPage/LoginPage.tsx delete mode 100644 packages/core/src/layout/LoginPage/index.ts diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 56faf4402b..b4d01e067b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -14,14 +14,9 @@ * limitations under the License. */ -import { - createApp, - AlertDisplay, - OAuthRequestDialog, - LoginPage, -} from '@backstage/core'; +import { createApp, AlertDisplay, OAuthRequestDialog } from '@backstage/core'; import React, { FC } from 'react'; -import { BrowserRouter as Router, Route } from 'react-router-dom'; +import { BrowserRouter as Router } from 'react-router-dom'; import Root from './components/Root'; import * as plugins from './plugins'; import apis from './apis'; @@ -40,7 +35,6 @@ const App: FC<{}> = () => ( - diff --git a/packages/core/src/layout/LoginPage/LoginPage.tsx b/packages/core/src/layout/LoginPage/LoginPage.tsx deleted file mode 100644 index 7bf14dba01..0000000000 --- a/packages/core/src/layout/LoginPage/LoginPage.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { FC, useState } from 'react'; -import GitHubIcon from '@material-ui/icons/GitHub'; -import { Page } from '../Page'; -import { Header } from '../Header'; -import { Content } from '../Content'; -import { ContentHeader } from '../ContentHeader'; -import { InfoCard } from '../InfoCard/InfoCard'; -import { - Grid, - Typography, - Button, - TextField, - List, - ListItem, - Link, -} from '@material-ui/core'; - -enum AuthType { - GitHub, -} - -export const LoginPage: FC<{}> = () => { - const [githubUsername, setGithubUsername] = useState(String); - const [githubPersonalAuthToken, setGithubPersonalAuthToken] = useState( - String, - ); - const [loginDetails, setLoginDetails] = useState(Object); - - const saveGithubInfo = (info: {}) => { - localStorage.setItem('githubLoginDetails', JSON.stringify(info)); - setLoginDetails(info); - }; - - const deleteGithubInfo = () => { - localStorage.removeItem('githubLoginDetails'); - setLoginDetails(undefined); - }; - - const handleTokenRegistration = (event: any) => { - switch (event.target.name) { - case 'github-username-tf': - setGithubUsername(event.target.value); - break; - case 'github-auth-tf': - setGithubPersonalAuthToken(event.target.value); - break; - default: - break; - } - }; - - const fetchGitHubToken = (username: String, token: String) => { - fetch('https://api.github.com/user', { - headers: new Headers({ - Authorization: `Basic ${btoa(`${username}:${token}`)}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }), - }) - .then(response => { - if (response.status === 200) return response.json(); - throw Error(`${response.status} ${response.statusText}`); - }) - .then(data => { - const info = { - username: username, - token: token, - name: data.name || data.login, - }; - saveGithubInfo(info); - }) - .catch(() => {}); - }; - - const validateUsernameAndToken = (username: String, token: String) => { - if (username === undefined || username === null || username === '') - return false; - - if (token === undefined || token === null || token === '') return false; - - return true; - }; - - const authenticate = (type: AuthType) => { - switch (type) { - case AuthType.GitHub: - { - const username = githubUsername; - const token = githubPersonalAuthToken; - if (validateUsernameAndToken(username, token)) - fetchGitHubToken(username, token); - } - break; - default: - break; - } - }; - - const LoginIndicator = () => { - const ls = localStorage.getItem('githubLoginDetails'); - if (ls !== null) { - const obj = ls || loginDetails ? JSON.parse(ls) : loginDetails; - return ( - - {`Welcome, ${obj.name}!`} -
- Logout -
- ); - } - return ( - - Welcome, guest! - - ); - }; - - return ( - -
- -
- - - - - - - GitHub - - - - - - - - - - - - - - - - -
- ); -}; diff --git a/packages/core/src/layout/LoginPage/index.ts b/packages/core/src/layout/LoginPage/index.ts deleted file mode 100644 index caa94bd6d7..0000000000 --- a/packages/core/src/layout/LoginPage/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export { LoginPage } from './LoginPage'; diff --git a/packages/core/src/layout/index.ts b/packages/core/src/layout/index.ts index c9dbae9ca2..e8341e1124 100644 --- a/packages/core/src/layout/index.ts +++ b/packages/core/src/layout/index.ts @@ -21,7 +21,6 @@ export * from './Header'; export * from './HeaderLabel'; export * from './HomepageTimer'; export * from './InfoCard'; -export * from './LoginPage'; export * from './Page'; export * from './Sidebar'; export * from './TabbedCard'; From 43d13afba5f3e430675899b46c4d63683c491531 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Sat, 30 May 2020 13:57:26 +0200 Subject: [PATCH 44/97] Add catalog-model dep --- plugins/catalog/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index cd8548bdde..0cdc5d2618 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -19,6 +19,7 @@ "dependencies": { "@backstage/core": "^0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/catalog-model": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", From a0a891a193d389b6f9c2dd8d89fc91f2b4d83230 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sat, 30 May 2020 21:37:54 +0900 Subject: [PATCH 45/97] Fix overlap issue with Sidebar and Dismissable banner (#1077) * Increase zIndex of Sidebar to be on top of the page As of now, the Sidebar is being overlapped by Dismissable banner component * Unset z-index of Dismissable Banner It inherits a z-index of 1400 from Material UI's snackbar component --- .../core/src/components/DismissableBanner/DismissableBanner.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx index 7190ee8725..b7a36c8b98 100644 --- a/packages/core/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx @@ -31,6 +31,7 @@ const useStyles = makeStyles((theme: Theme) => ({ marginTop: -theme.spacing(3), display: 'flex', flexFlow: 'row nowrap', + zIndex: 'unset', }, icon: { fontSize: 20, From a8e42631d2cf77a616c0e9645ce5468f32095f4a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 30 May 2020 14:39:30 +0200 Subject: [PATCH 46/97] build(deps): bump rollup-plugin-dts from 1.4.6 to 1.4.7 (#1021) Bumps [rollup-plugin-dts](https://github.com/Swatinem/rollup-plugin-dts) from 1.4.6 to 1.4.7. - [Release notes](https://github.com/Swatinem/rollup-plugin-dts/releases) - [Changelog](https://github.com/Swatinem/rollup-plugin-dts/blob/master/CHANGELOG.md) - [Commits](https://github.com/Swatinem/rollup-plugin-dts/compare/v1.4.6...v1.4.7) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b34d5505d2..77528ae6f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18353,9 +18353,9 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: inherits "^2.0.1" rollup-plugin-dts@^1.4.6: - version "1.4.6" - resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.6.tgz#26e3da11ec647cfffee9658b63fa41d67e7840b9" - integrity sha512-1o5+eI97Ne8zXJrgdasn/xGi0xKuovCQwZRtPI2Lfl/c6qa9jQTFbn60NwOx3gWJ89K265/6kpDuahnBbplyWA== + version "1.4.7" + resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.7.tgz#6255147ac777314c0725a1efcb42df10fe282243" + integrity sha512-QkunbJ96yUNkW95k/Vd6SdTjCbWSG0rMVUtpHSCwfg078Z7vbDaBnfz/gkSqR5h8WFMxoccBT4aodHm6387Jvg== optionalDependencies: "@babel/code-frame" "^7.8.3" From 9ccf617b82e9ff6f95429d9c3bd7b995070aa75d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 May 2020 23:01:35 +0200 Subject: [PATCH 47/97] Add routes for add/delete entity to the catalog --- packages/backend/src/plugins/catalog.ts | 2 +- .../catalog/DatabaseEntitiesCatalog.test.ts | 110 ++++++++++++++++++ .../src/catalog/DatabaseEntitiesCatalog.ts | 75 +++++++++--- .../catalog/DatabaseLocationsCatalog.test.ts | 17 +-- .../src/catalog/DatabaseLocationsCatalog.ts | 5 +- .../src/catalog/StaticEntitiesCatalog.ts | 23 ++-- plugins/catalog-backend/src/catalog/index.ts | 14 ++- plugins/catalog-backend/src/catalog/types.ts | 9 +- ...atabase.test.ts => CommonDatabase.test.ts} | 52 ++++----- .../{Database.ts => CommonDatabase.ts} | 75 +++++------- .../src/database/DatabaseManager.test.ts | 10 +- .../src/database/DatabaseManager.ts | 79 +++++++++---- plugins/catalog-backend/src/database/index.ts | 12 +- .../src/database/search.test.ts | 4 +- .../catalog-backend/src/database/search.ts | 4 +- plugins/catalog-backend/src/database/types.ts | 81 ++++++++++++- .../src/ingestion/IngestionModels.ts | 4 +- .../src/service/router.test.ts | 101 +++++++++++++++- plugins/catalog-backend/src/service/router.ts | 15 ++- plugins/catalog-backend/src/service/util.ts | 22 +++- 20 files changed, 545 insertions(+), 169 deletions(-) create mode 100644 plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts rename plugins/catalog-backend/src/database/{Database.test.ts => CommonDatabase.test.ts} (90%) rename plugins/catalog-backend/src/database/{Database.ts => CommonDatabase.ts} (88%) diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 7e843cc80b..9f5315fa08 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -41,7 +41,7 @@ export default async function ({ logger, database }: PluginEnvironment) { 10000, ); - const entitiesCatalog = new DatabaseEntitiesCatalog(db); + const entitiesCatalog = new DatabaseEntitiesCatalog(db, policy); const locationsCatalog = new DatabaseLocationsCatalog(db, ingestion); return await createRouter({ entitiesCatalog, locationsCatalog, logger }); diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts new file mode 100644 index 0000000000..d897584c95 --- /dev/null +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Entity, EntityPolicy } from '@backstage/catalog-model'; +import type { Database } from '../database'; +import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; + +describe('DatabaseEntitiesCatalog', () => { + let db: Database; + let policy: EntityPolicy; + + beforeEach(() => { + // Since the database has a large API surface, we just leave it empty and + // let the tests insert whatever methods they need to call + db = ({ + transaction: jest.fn(async f => f('mock_tx')), + } as unknown) as Database; + policy = { enforce: jest.fn(async x => x) }; + }); + + describe('addOrUpdateEntity', () => { + it('adds when no given uid and no matching by name', async () => { + const entity: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + namespace: 'd', + }, + }; + + db.entities = jest.fn().mockResolvedValue([]); + db.addEntity = jest.fn().mockResolvedValue({ entity }); + + const catalog = new DatabaseEntitiesCatalog(db, policy); + const result = await catalog.addOrUpdateEntity(entity); + + expect(policy.enforce).toBeCalledWith(entity); + expect(db.entities).toHaveBeenCalledTimes(1); + expect(db.addEntity).toHaveBeenCalledTimes(1); + expect(result).toBe(entity); + }); + + it('updates when given uid', async () => { + const entity: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + uid: 'uuuu', + name: 'c', + namespace: 'd', + }, + }; + + db.entities = jest.fn().mockResolvedValue([]); + db.updateEntity = jest.fn().mockResolvedValue({ entity }); + + const catalog = new DatabaseEntitiesCatalog(db, policy); + const result = await catalog.addOrUpdateEntity(entity); + + expect(policy.enforce).toBeCalledWith(entity); + expect(db.entities).toHaveBeenCalledTimes(0); + expect(db.updateEntity).toHaveBeenCalledTimes(1); + expect(result).toBe(entity); + }); + + it('update when no given uid and matching by name', async () => { + const added: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + namespace: 'd', + }, + }; + const existing: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + namespace: 'd', + }, + }; + + db.entities = jest.fn().mockResolvedValue([{ entity: existing }]); + db.updateEntity = jest.fn().mockResolvedValue({ entity: added }); + + const catalog = new DatabaseEntitiesCatalog(db, policy); + const result = await catalog.addOrUpdateEntity(added); + + expect(policy.enforce).toBeCalledWith(added); + expect(db.entities).toHaveBeenCalledTimes(1); + expect(db.updateEntity).toHaveBeenCalledTimes(1); + expect(result).toEqual(existing); + }); + }); +}); diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 972410a639..d13ac9a1ba 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -14,12 +14,15 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { Database } from '../database'; -import { EntitiesCatalog, EntityFilters } from './types'; +import type { Entity, EntityPolicy } from '@backstage/catalog-model'; +import type { Database, DbEntityResponse, EntityFilters } from '../database'; +import type { EntitiesCatalog } from './types'; export class DatabaseEntitiesCatalog implements EntitiesCatalog { - constructor(private readonly database: Database) {} + constructor( + private readonly database: Database, + private readonly policy: EntityPolicy, + ) {} async entities(filters?: EntityFilters): Promise { const items = await this.database.transaction(tx => @@ -41,19 +44,59 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { name: string, namespace: string | undefined, ): Promise { - const matches = await this.database.transaction(tx => - this.database.entities(tx, [ - { key: 'kind', values: [kind] }, - { key: 'name', values: [name] }, - { - key: 'namespace', - values: - !namespace || namespace === 'default' - ? [null, 'default'] - : [namespace], - }, - ]), + return await this.database.transaction(tx => + this.entityByNameInternal(tx, kind, name, namespace), ); + } + + async addOrUpdateEntity(entity: Entity): Promise { + await this.policy.enforce(entity); + return await this.database.transaction(async tx => { + let response: DbEntityResponse; + + if (entity.metadata.uid) { + response = await this.database.updateEntity(tx, { entity }); + } else { + const existing = await this.entityByNameInternal( + tx, + entity.kind, + entity.metadata.name, + entity.metadata.namespace, + ); + if (existing) { + response = await this.database.updateEntity(tx, { entity }); + } else { + response = await this.database.addEntity(tx, { entity }); + } + } + + return response.entity; + }); + } + + async removeEntityByUid(uid: string): Promise { + return await this.database.transaction(async tx => { + await this.database.removeEntity(tx, uid); + }); + } + + private async entityByNameInternal( + tx: unknown, + kind: string, + name: string, + namespace: string | undefined, + ): Promise { + const matches = await this.database.entities(tx, [ + { key: 'kind', values: [kind] }, + { key: 'name', values: [name] }, + { + key: 'namespace', + values: + !namespace || namespace === 'default' + ? [null, 'default'] + : [namespace], + }, + ]); return matches.length ? matches[0].entity : undefined; } diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts index 56a3b3828f..443b3bf6b0 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -14,11 +14,12 @@ * limitations under the License. */ import { getVoidLogger } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; -import knex from 'knex'; +import type { Entity } from '@backstage/catalog-model'; +import Knex from 'knex'; import path from 'path'; -import { Database } from '../database'; -import { IngestionModel } from '../ingestion/types'; +import { CommonDatabase } from '../database'; +import type { Database } from '../database'; +import type { IngestionModel } from '../ingestion/types'; import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; class MockIngestionModel implements IngestionModel { @@ -36,12 +37,12 @@ class MockIngestionModel implements IngestionModel { } describe('DatabaseLocationsCatalog', () => { - const database = knex({ + const knex = Knex({ client: 'sqlite3', connection: ':memory:', useNullAsDefault: true, }); - database.client.pool.on('createSuccess', (_eventId: any, resource: any) => { + knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => { resource.run('PRAGMA foreign_keys = ON', () => {}); }); let db: Database; @@ -49,11 +50,11 @@ describe('DatabaseLocationsCatalog', () => { let ingestionModel: IngestionModel; beforeEach(async () => { - await database.migrate.latest({ + await knex.migrate.latest({ directory: path.resolve(__dirname, '../database/migrations'), loadExtensions: ['.ts'], }); - db = new Database(database, getVoidLogger()); + db = new CommonDatabase(knex, getVoidLogger()); ingestionModel = new MockIngestionModel(); catalog = new DatabaseLocationsCatalog(db, ingestionModel); }); diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index b5841c8c2c..ae910e0b9b 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import { Database, DatabaseLocationUpdateLogEvent } from '../database'; +import type { Database } from '../database'; +import { DatabaseLocationUpdateLogEvent } from '../database/types'; import { IngestionModel } from '../ingestion/types'; import { AddLocation, Location, - LocationsCatalog, LocationResponse, + LocationsCatalog, } from './types'; export class DatabaseLocationsCatalog implements LocationsCatalog { diff --git a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts index 64ac5f57d5..22bbd2e1a3 100644 --- a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts @@ -14,10 +14,9 @@ * limitations under the License. */ -import { NotFoundError } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; +import type { Entity } from '@backstage/catalog-model'; import lodash from 'lodash'; -import { EntitiesCatalog } from './types'; +import type { EntitiesCatalog } from './types'; export class StaticEntitiesCatalog implements EntitiesCatalog { private _entities: Entity[]; @@ -32,10 +31,7 @@ export class StaticEntitiesCatalog implements EntitiesCatalog { async entityByUid(uid: string): Promise { const item = this._entities.find(e => uid === e.metadata.uid); - if (!item) { - throw new NotFoundError('Entity cannot be found'); - } - return lodash.cloneDeep(item); + return item ? lodash.cloneDeep(item) : undefined; } async entityByName( @@ -49,9 +45,14 @@ export class StaticEntitiesCatalog implements EntitiesCatalog { name === e.metadata.name && namespace === e.metadata.namespace, ); - if (!item) { - throw new NotFoundError('Entity cannot be found'); - } - return lodash.cloneDeep(item); + return item ? lodash.cloneDeep(item) : undefined; + } + + async addOrUpdateEntity(): Promise { + throw new Error('Not supported'); + } + + async removeEntityByUid(): Promise { + throw new Error('Not supported'); } } diff --git a/plugins/catalog-backend/src/catalog/index.ts b/plugins/catalog-backend/src/catalog/index.ts index 58ae531944..6768268f34 100644 --- a/plugins/catalog-backend/src/catalog/index.ts +++ b/plugins/catalog-backend/src/catalog/index.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -export * from './DatabaseEntitiesCatalog'; -export * from './DatabaseLocationsCatalog'; -export * from './StaticEntitiesCatalog'; -export * from './types'; +export { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; +export { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; +export { StaticEntitiesCatalog } from './StaticEntitiesCatalog'; +export { addLocationSchema } from './types'; +export type { + AddLocation, + EntitiesCatalog, + Location, + LocationsCatalog, +} from './types'; diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 5f55de251f..61fba33ebf 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -16,17 +16,12 @@ import { Entity } from '@backstage/catalog-model'; import * as yup from 'yup'; +import type { EntityFilters } from '../database'; // // Entities // -export type EntityFilter = { - key: string; - values: (string | null)[]; -}; -export type EntityFilters = EntityFilter[]; - export type EntitiesCatalog = { entities(filters?: EntityFilters): Promise; entityByUid(uid: string): Promise; @@ -35,6 +30,8 @@ export type EntitiesCatalog = { namespace: string | undefined, name: string, ): Promise; + addOrUpdateEntity(entity: Entity): Promise; + removeEntityByUid(uid: string): Promise; }; // diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts similarity index 90% rename from plugins/catalog-backend/src/database/Database.test.ts rename to plugins/catalog-backend/src/database/CommonDatabase.test.ts index 6f15440fff..3b783a7691 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts @@ -19,33 +19,33 @@ import { getVoidLogger, NotFoundError, } from '@backstage/backend-common'; +import type { Entity } from '@backstage/catalog-model'; import Knex from 'knex'; import path from 'path'; -import { +import { CommonDatabase } from './CommonDatabase'; +import { DatabaseLocationUpdateLogStatus } from './types'; +import type { + AddDatabaseLocation, DbEntityRequest, DbEntityResponse, - Database, - AddDatabaseLocation, DbLocationsRow, DbLocationsRowWithStatus, - DatabaseLocationUpdateLogStatus, -} from '.'; -import { Entity } from '@backstage/catalog-model'; +} from './types'; -describe('Database', () => { - let database: Knex; +describe('CommonDatabase', () => { + let knex: Knex; let entityRequest: DbEntityRequest; let entityResponse: DbEntityResponse; beforeEach(async () => { - database = Knex({ + knex = Knex({ client: 'sqlite3', connection: ':memory:', useNullAsDefault: true, }); - await database.raw('PRAGMA foreign_keys = ON'); - await database.migrate.latest({ + await knex.raw('PRAGMA foreign_keys = ON'); + await knex.migrate.latest({ directory: path.resolve(__dirname, 'migrations'), loadExtensions: ['.ts'], }); @@ -86,7 +86,7 @@ describe('Database', () => { }); it('manages locations', async () => { - const db = new Database(database, getVoidLogger()); + const db = new CommonDatabase(knex, getVoidLogger()); const input: AddDatabaseLocation = { type: 'a', target: 'b' }; const output: DbLocationsRowWithStatus = { id: expect.anything(), @@ -114,7 +114,7 @@ describe('Database', () => { it('instead of adding second location with the same target, returns existing one', async () => { // Prepare - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const input: AddDatabaseLocation = { type: 'a', target: 'b' }; const output1: DbLocationsRow = await catalog.addLocation(input); @@ -130,7 +130,7 @@ describe('Database', () => { describe('addEntity', () => { it('happy path: adds entity to empty database', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); @@ -139,7 +139,7 @@ describe('Database', () => { }); it('rejects adding the same-named entity twice', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); await catalog.transaction(tx => catalog.addEntity(tx, entityRequest)); await expect( catalog.transaction(tx => catalog.addEntity(tx, entityRequest)), @@ -147,7 +147,7 @@ describe('Database', () => { }); it('accepts adding the same-named entity twice if on different namespaces', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); entityRequest.entity.metadata.namespace = 'namespace1'; await catalog.transaction(tx => catalog.addEntity(tx, entityRequest)); entityRequest.entity.metadata.namespace = 'namespace2'; @@ -159,7 +159,7 @@ describe('Database', () => { describe('locationHistory', () => { it('outputs the history correctly', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const location: AddDatabaseLocation = { type: 'a', target: 'b' }; const { id: locationId } = await catalog.addLocation(location); @@ -198,7 +198,7 @@ describe('Database', () => { describe('updateEntity', () => { it('can read and no-op-update an entity', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); @@ -220,7 +220,7 @@ describe('Database', () => { }); it('can update name if uid matches', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); @@ -232,7 +232,7 @@ describe('Database', () => { }); it('can update fields if kind, name, and namespace match', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); @@ -246,7 +246,7 @@ describe('Database', () => { }); it('rejects if kind, name, but not namespace match', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); @@ -262,7 +262,7 @@ describe('Database', () => { }); it('fails to update an entity if etag does not match', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); @@ -275,7 +275,7 @@ describe('Database', () => { }); it('fails to update an entity if generation does not match', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const added = await catalog.transaction(tx => catalog.addEntity(tx, entityRequest), ); @@ -290,7 +290,7 @@ describe('Database', () => { describe('entities', () => { it('can get all entities with empty filters list', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const e1: Entity = { apiVersion: 'a', kind: 'k1', @@ -325,7 +325,7 @@ describe('Database', () => { }); it('can get all specific entities for matching filters (naive case)', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const entities: Entity[] = [ { apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } }, { @@ -364,7 +364,7 @@ describe('Database', () => { }); it('can get all specific entities for matching filters with nulls (both missing and literal null value)', async () => { - const catalog = new Database(database, getVoidLogger()); + const catalog = new CommonDatabase(knex, getVoidLogger()); const entities: Entity[] = [ { apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } }, { diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts similarity index 88% rename from plugins/catalog-backend/src/database/Database.ts rename to plugins/catalog-backend/src/database/CommonDatabase.ts index ea823a3da7..2dac08b9bc 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -19,15 +19,15 @@ import { InputError, NotFoundError, } from '@backstage/backend-common'; -import { Entity, EntityMeta } from '@backstage/catalog-model'; +import type { Entity, EntityMeta } from '@backstage/catalog-model'; import Knex from 'knex'; import lodash from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { Logger } from 'winston'; -import { EntityFilters } from '../catalog'; +import type { Logger } from 'winston'; import { buildEntitySearch } from './search'; -import { +import type { AddDatabaseLocation, + Database, DatabaseLocationUpdateLogEvent, DatabaseLocationUpdateLogStatus, DbEntitiesRow, @@ -36,6 +36,7 @@ import { DbEntityResponse, DbLocationsRow, DbLocationsRowWithStatus, + EntityFilters, } from './types'; function getStrippedMetadata(metadata: EntityMeta): EntityMeta { @@ -121,27 +122,13 @@ function generateEtag(): string { return Buffer.from(uuidv4(), 'utf8').toString('base64').replace(/[^\w]/g, ''); } -/** - * An abstraction on top of the underlying database, wrapping the basic CRUD - * needs. - */ -export class Database { +export class CommonDatabase implements Database { constructor( private readonly database: Knex, private readonly logger: Logger, ) {} - /** - * Runs a transaction. - * - * The callback is expected to make calls back into this class. When it - * completes, the transaction is closed. - * - * @param fn The callback that implements the transaction - */ - async transaction( - fn: (tx: Knex.Transaction) => Promise, - ): Promise { + async transaction(fn: (tx: unknown) => Promise): Promise { try { return await this.database.transaction(fn); } catch (e) { @@ -158,17 +145,12 @@ export class Database { } } - /** - * Adds a new entity to the catalog. - * - * @param tx An ongoing transaction - * @param request The entity being added - * @returns The added entity, with uid, etag and generation set - */ async addEntity( - tx: Knex.Transaction, + txOpaque: unknown, request: DbEntityRequest, ): Promise { + const tx = txOpaque as Knex.Transaction; + if (request.entity.metadata.uid !== undefined) { throw new InputError('May not specify uid for new entities'); } else if (request.entity.metadata.etag !== undefined) { @@ -198,25 +180,12 @@ export class Database { return { locationId: request.locationId, entity: newEntity }; } - /** - * Updates an existing entity in the catalog. - * - * The given entity must contain enough information to identify an already - * stored entity in the catalog - either by uid, or by kind + namespace + - * name. If no matching entity is found, the operation fails. - * - * If etag or generation are given, they are taken into account. Attempts to - * update a matching entity, but where the etag and/or generation are not - * equal to the passed values, will fail. - * - * @param tx An ongoing transaction - * @param request The entity being updated - * @returns The updated entity - */ async updateEntity( - tx: Knex.Transaction, + txOpaque: unknown, request: DbEntityRequest, ): Promise { + const tx = txOpaque as Knex.Transaction; + const { kind } = request.entity; const { uid, @@ -310,9 +279,11 @@ export class Database { } async entities( - tx: Knex.Transaction, + txOpaque: unknown, filters?: EntityFilters, ): Promise { + const tx = txOpaque as Knex.Transaction; + let builder = tx('entities'); for (const [index, filter] of (filters ?? []).entries()) { builder = builder @@ -337,11 +308,13 @@ export class Database { } async entity( - tx: Knex.Transaction, + txOpaque: unknown, kind: string, name: string, namespace?: string, ): Promise { + const tx = txOpaque as Knex.Transaction; + const rows = await tx('entities') .where({ kind, name, namespace: namespace || null }) .select(); @@ -353,6 +326,16 @@ export class Database { return toEntityResponse(rows[0]); } + async removeEntity(txOpaque: unknown, uid: string): Promise { + const tx = txOpaque as Knex.Transaction; + + const result = await tx('entities').where({ id: uid }).del(); + + if (!result) { + throw new NotFoundError(`Found no entity with ID ${uid}`); + } + } + async addLocation(location: AddDatabaseLocation): Promise { return await this.database.transaction(async tx => { const existingLocation = await tx('locations') diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts index 6e18eaf419..0c28524f8d 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.test.ts @@ -15,16 +15,16 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import type { Entity, EntityPolicy } from '@backstage/catalog-model'; import Knex from 'knex'; -import { Database } from './Database'; +import type { IngestionModel } from '../ingestion/types'; import { DatabaseManager } from './DatabaseManager'; -import { - DatabaseLocationUpdateLogStatus, +import { DatabaseLocationUpdateLogStatus } from './types'; +import type { + Database, DbLocationsRow, DbLocationsRowWithStatus, } from './types'; -import { EntityPolicy, Entity } from '@backstage/catalog-model'; -import { IngestionModel } from '..'; describe('DatabaseManager', () => { describe('refreshLocations', () => { diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index 05b6eaef8d..4268dc4146 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -14,25 +14,26 @@ * limitations under the License. */ -import { Entity, EntityPolicy } from '@backstage/catalog-model'; +import type { Entity, EntityPolicy } from '@backstage/catalog-model'; import Knex from 'knex'; import lodash from 'lodash'; import path from 'path'; import { Logger } from 'winston'; -import { IngestionModel } from '../ingestion/types'; -import { Database } from './Database'; -import { DatabaseLocationUpdateLogStatus, DbEntityRequest } from './types'; +import type { IngestionModel } from '../ingestion/types'; +import { CommonDatabase } from './CommonDatabase'; +import { DatabaseLocationUpdateLogStatus } from './types'; +import type { Database, DbEntityRequest } from './types'; export class DatabaseManager { public static async createDatabase( - database: Knex, + knex: Knex, logger: Logger, ): Promise { - await database.migrate.latest({ + await knex.migrate.latest({ directory: path.resolve(__dirname, 'migrations'), loadExtensions: ['.js'], }); - return new Database(database, logger); + return new CommonDatabase(knex, logger); } private static async logUpdateSuccess( @@ -158,22 +159,58 @@ export class DatabaseManager { }); } - private static entitiesAreEqual(first: Entity, second: Entity) { - const firstClone = lodash.cloneDeep(first); - const secondClone = lodash.cloneDeep(second); + private static entitiesAreEqual(previous: Entity, next: Entity) { + if ( + previous.apiVersion !== next.apiVersion || + previous.kind !== next.kind || + !lodash.isEqual(previous.spec, next.spec) // Accept that {} !== undefined + ) { + return false; + } + + // Since the next annotations get merged into the previous, extract only + // the overlapping keys and check if their values match. + if (next.metadata.annotations) { + if (!previous.metadata.annotations) { + return false; + } + if ( + !lodash.isEqual( + next.metadata.annotations, + lodash.pick( + previous.metadata.annotations, + Object.keys(next.metadata.annotations), + ), + ) + ) { + return false; + } + } + + const e1 = lodash.cloneDeep(previous); + const e2 = lodash.cloneDeep(next); + + if (!e1.metadata.labels) { + e1.metadata.labels = {}; + } + if (!e2.metadata.labels) { + e2.metadata.labels = {}; + } // Remove generated fields - if (firstClone.metadata) { - delete firstClone.metadata.uid; - delete firstClone.metadata.etag; - delete firstClone.metadata.generation; - } - if (secondClone.metadata) { - delete secondClone.metadata.uid; - delete secondClone.metadata.etag; - delete secondClone.metadata.generation; - } + delete e1.metadata.uid; + delete e1.metadata.etag; + delete e1.metadata.generation; + delete e2.metadata.uid; + delete e2.metadata.etag; + delete e2.metadata.generation; - return lodash.isEqual(firstClone, secondClone); + // Remove already compared things + delete e1.metadata.annotations; + delete e1.spec; + delete e2.metadata.annotations; + delete e2.spec; + + return lodash.isEqual(e1, e2); } } diff --git a/plugins/catalog-backend/src/database/index.ts b/plugins/catalog-backend/src/database/index.ts index 616808fb67..565a41cfb2 100644 --- a/plugins/catalog-backend/src/database/index.ts +++ b/plugins/catalog-backend/src/database/index.ts @@ -14,6 +14,12 @@ * limitations under the License. */ -export * from './Database'; -export * from './DatabaseManager'; -export * from './types'; +export { CommonDatabase } from './CommonDatabase'; +export { DatabaseManager } from './DatabaseManager'; +export type { + Database, + DbEntityRequest, + DbEntityResponse, + EntityFilter, + EntityFilters, +} from './types'; diff --git a/plugins/catalog-backend/src/database/search.test.ts b/plugins/catalog-backend/src/database/search.test.ts index 8f011fb250..38a2d40e74 100644 --- a/plugins/catalog-backend/src/database/search.test.ts +++ b/plugins/catalog-backend/src/database/search.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import type { Entity } from '@backstage/catalog-model'; import { buildEntitySearch, visitEntityPart } from './search'; -import { DbEntitiesSearchRow } from './types'; +import type { DbEntitiesSearchRow } from './types'; describe('search', () => { describe('visitEntityPart', () => { diff --git a/plugins/catalog-backend/src/database/search.ts b/plugins/catalog-backend/src/database/search.ts index 87fc59185d..c14acb6661 100644 --- a/plugins/catalog-backend/src/database/search.ts +++ b/plugins/catalog-backend/src/database/search.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { DbEntitiesSearchRow } from './types'; +import type { Entity } from '@backstage/catalog-model'; +import type { DbEntitiesSearchRow } from './types'; // Search entries that start with these prefixes, also get a shorthand without // that prefix diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 0b9485242f..f69a7353c7 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import type { Entity } from '@backstage/catalog-model'; import * as yup from 'yup'; export type DbEntitiesRow = { @@ -83,3 +83,82 @@ export type DatabaseLocationUpdateLogEvent = { created_at?: string; message?: string; }; + +export type EntityFilter = { + key: string; + values: (string | null)[]; +}; +export type EntityFilters = EntityFilter[]; + +/** + * An abstraction on top of the underlying database, wrapping the basic CRUD + * needs. + */ +export type Database = { + /** + * Runs a transaction. + * + * The callback is expected to make calls back into this class. When it + * completes, the transaction is closed. + * + * @param fn The callback that implements the transaction + */ + transaction(fn: (tx: unknown) => Promise): Promise; + + /** + * Adds a new entity to the catalog. + * + * @param tx An ongoing transaction + * @param request The entity being added + * @returns The added entity, with uid, etag and generation set + */ + addEntity(tx: unknown, request: DbEntityRequest): Promise; + + /** + * Updates an existing entity in the catalog. + * + * The given entity must contain enough information to identify an already + * stored entity in the catalog - either by uid, or by kind + namespace + + * name. If no matching entity is found, the operation fails. + * + * If etag or generation are given, they are taken into account. Attempts to + * update a matching entity, but where the etag and/or generation are not + * equal to the passed values, will fail. + * + * @param tx An ongoing transaction + * @param request The entity being updated + * @returns The updated entity + */ + updateEntity( + tx: unknown, + request: DbEntityRequest, + ): Promise; + + entities(tx: unknown, filters?: EntityFilters): Promise; + + entity( + tx: unknown, + kind: string, + name: string, + namespace?: string, + ): Promise; + + removeEntity(tx: unknown, uid: string): Promise; + + addLocation(location: AddDatabaseLocation): Promise; + + removeLocation(id: string): Promise; + + location(id: string): Promise; + + locations(): Promise; + + locationHistory(id: string): Promise; + + addLocationUpdateLogEvent( + locationId: string, + status: DatabaseLocationUpdateLogStatus, + entityName?: string, + message?: string, + ): Promise; +}; diff --git a/plugins/catalog-backend/src/ingestion/IngestionModels.ts b/plugins/catalog-backend/src/ingestion/IngestionModels.ts index def6f1fa5c..8febdecd18 100644 --- a/plugins/catalog-backend/src/ingestion/IngestionModels.ts +++ b/plugins/catalog-backend/src/ingestion/IngestionModels.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { EntityPolicy, EntityPolicies } from '@backstage/catalog-model'; +import { EntityPolicies, EntityPolicy } from '@backstage/catalog-model'; +import { DescriptorParsers } from './descriptor'; import { DescriptorParser, ReaderOutput } from './descriptor/parsers/types'; import { LocationReader, LocationReaders } from './source'; import { IngestionModel } from './types'; -import { DescriptorParsers } from './descriptor'; export class IngestionModels implements IngestionModel { private readonly reader: LocationReader; diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 8c7946816c..4092ed1395 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; +import { getVoidLogger, NotFoundError } from '@backstage/backend-common'; +import type { Entity } from '@backstage/catalog-model'; import express from 'express'; import request from 'supertest'; import { EntitiesCatalog, Location, LocationsCatalog } from '../catalog'; @@ -25,6 +25,9 @@ class MockEntitiesCatalog implements EntitiesCatalog { entities = jest.fn(); entityByUid = jest.fn(); entityByName = jest.fn(); + addEntity = jest.fn(); + addOrUpdateEntity = jest.fn(); + removeEntityByUid = jest.fn(); } class MockLocationsCatalog implements LocationsCatalog { @@ -36,7 +39,7 @@ class MockLocationsCatalog implements LocationsCatalog { } describe('createRouter', () => { - describe('entities', () => { + describe('GET /entities', () => { it('happy path: lists entities', async () => { const entities: Entity[] = [ { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, @@ -77,7 +80,7 @@ describe('createRouter', () => { }); }); - describe('entityByUid', () => { + describe('GET /entities/by-uid/:uid', () => { it('can fetch entity by uid', async () => { const entity: Entity = { apiVersion: 'a', @@ -118,7 +121,7 @@ describe('createRouter', () => { }); }); - describe('entityByName', () => { + describe('GET /entities/by-name/:kind/:namespace/:name', () => { it('can fetch entity by name', async () => { const entity: Entity = { apiVersion: 'a', @@ -160,7 +163,91 @@ describe('createRouter', () => { }); }); - describe('locations', () => { + describe('POST /entities', () => { + it('requires a body', async () => { + const catalog = new MockEntitiesCatalog(); + const router = await createRouter({ + entitiesCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app) + .post('/entities') + .set('Content-Type', 'application/json') + .send(); + + expect(response.status).toEqual(400); + expect(response.text).toMatch(/body/); + expect(catalog.addOrUpdateEntity).not.toHaveBeenCalled(); + }); + + it('passes the body down', async () => { + const entity: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + namespace: 'd', + }, + }; + + const catalog = new MockEntitiesCatalog(); + catalog.addOrUpdateEntity.mockResolvedValue(entity); + + const router = await createRouter({ + entitiesCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app) + .post('/entities') + .send(entity) + .set('Content-Type', 'application/json'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(entity); + expect(catalog.addOrUpdateEntity).toHaveBeenCalledTimes(1); + expect(catalog.addOrUpdateEntity).toHaveBeenNthCalledWith(1, entity); + }); + }); + + describe('DELETE /entities/by-uid/:uid', () => { + it('can remove', async () => { + const catalog = new MockEntitiesCatalog(); + catalog.removeEntityByUid.mockResolvedValue(undefined); + + const router = await createRouter({ + entitiesCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app).delete('/entities/by-uid/apa'); + + expect(response.status).toEqual(204); + expect(catalog.removeEntityByUid).toHaveBeenCalledTimes(1); + }); + + it('responds with a 404 for missing entities', async () => { + const catalog = new MockEntitiesCatalog(); + catalog.removeEntityByUid.mockRejectedValue(new NotFoundError('nope')); + + const router = await createRouter({ + entitiesCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app).delete('/entities/by-uid/apa'); + + expect(response.status).toEqual(404); + expect(catalog.removeEntityByUid).toHaveBeenCalledTimes(1); + }); + }); + + describe('GET /locations', () => { it('happy path: lists locations', async () => { const locations: Location[] = [{ id: 'a', type: 'b', target: 'c' }]; @@ -178,7 +265,9 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(response.body).toEqual(locations); }); + }); + describe('POST /locations', () => { it('rejects malformed locations', async () => { const location = ({ id: 'a', diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 32bba44984..f5adbd84ca 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -15,16 +15,17 @@ */ import { errorHandler, InputError } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { addLocationSchema, EntitiesCatalog, - EntityFilters, LocationsCatalog, } from '../catalog'; -import { validateRequestBody } from './util'; +import { EntityFilters } from '../database'; +import { requireRequestBody, validateRequestBody } from './util'; export interface RouterOptions { entitiesCatalog?: EntitiesCatalog; @@ -47,6 +48,11 @@ export async function createRouter( const entities = await entitiesCatalog.entities(filters); res.status(200).send(entities); }) + .post('/entities', async (req, res) => { + const body = await requireRequestBody(req); + const result = await entitiesCatalog.addOrUpdateEntity(body as Entity); + res.status(200).send(result); + }) .get('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; const entity = await entitiesCatalog.entityByUid(uid); @@ -55,6 +61,11 @@ export async function createRouter( } res.status(200).send(entity); }) + .delete('/entities/by-uid/:uid', async (req, res) => { + const { uid } = req.params; + await entitiesCatalog.removeEntityByUid(uid); + res.status(204).send(); + }) .get('/entities/by-name/:kind/:namespace/:name', async (req, res) => { const { kind, namespace, name } = req.params; const entity = await entitiesCatalog.entityByName( diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 4c37154c48..39692030e3 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -16,12 +16,10 @@ import { InputError } from '@backstage/backend-common'; import { Request } from 'express'; +import lodash from 'lodash'; import yup from 'yup'; -export async function validateRequestBody( - req: Request, - schema: yup.Schema, -): Promise { +export async function requireRequestBody(req: Request): Promise { const contentType = req.header('content-type'); if (!contentType) { throw new InputError('Content-Type missing'); @@ -32,13 +30,27 @@ export async function validateRequestBody( const body = req.body; if (!body) { throw new InputError('Missing request body'); + } else if (!lodash.isPlainObject(body)) { + throw new InputError('Expected body to be a JSON object'); + } else if (Object.keys(body).length === 0) { + // Because of how express.json() translates the empty body to {} + throw new InputError('Empty request body'); } + return body; +} + +export async function validateRequestBody( + req: Request, + schema: yup.Schema, +): Promise { + const body = await requireRequestBody(req); + try { await schema.validate(body, { strict: true }); } catch (e) { throw new InputError(`Malformed request: ${e}`); } - return body as T; + return (body as unknown) as T; } From 43795b857664d21d2c700cc4bada82431abe42ba Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 31 May 2020 19:37:38 +0200 Subject: [PATCH 48/97] packages/dev-utils: added addRootChild to DevAppBuilder --- packages/dev-utils/src/devApp/render.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 72c9581a84..5a28a43e8b 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -15,7 +15,7 @@ */ import { hot } from 'react-hot-loader/root'; -import React, { FC, ComponentType } from 'react'; +import React, { FC, ComponentType, ReactNode } from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import BookmarkIcon from '@material-ui/icons/Bookmark'; @@ -43,6 +43,7 @@ type BackstagePlugin = ReturnType; class DevAppBuilder { private readonly plugins = new Array(); private readonly factories = new Array>(); + private readonly rootChildren = new Array(); /** * Register one or more plugins to render in the dev app @@ -62,6 +63,16 @@ class DevAppBuilder { return this; } + /** + * Adds a React node to place just inside the App Provider. + * + * Useful for adding more global components like the AlertDisplay. + */ + addRootChild(node: ReactNode): DevAppBuilder { + this.rootChildren.push(node); + return this; + } + /** * Build a DevApp component using the resources registered so far */ @@ -79,6 +90,7 @@ class DevAppBuilder { return ( + {this.rootChildren} {sidebar} From 6f955a536678eb91c43405f751549666a72b1144 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 31 May 2020 19:40:58 +0200 Subject: [PATCH 49/97] packages/dev-utils: add oauthRequestApi factory + dialog --- packages/dev-utils/src/devApp/apiFactories.ts | 8 ++++++++ packages/dev-utils/src/devApp/render.tsx | 2 ++ 2 files changed, 10 insertions(+) diff --git a/packages/dev-utils/src/devApp/apiFactories.ts b/packages/dev-utils/src/devApp/apiFactories.ts index 162375cf1c..967918d87a 100644 --- a/packages/dev-utils/src/devApp/apiFactories.ts +++ b/packages/dev-utils/src/devApp/apiFactories.ts @@ -22,6 +22,8 @@ import { createApiFactory, ErrorAlerter, AlertApiForwarder, + oauthRequestApiRef, + OAuthRequestManager, } from '@backstage/core'; // TODO(rugvip): We should likely figure out how to reuse all of these between apps @@ -41,3 +43,9 @@ export const errorApiFactory = createApiFactory({ factory: ({ alertApi }) => new ErrorAlerter(alertApi, new ErrorApiForwarder()), }); + +export const oauthRequestApiFactory = createApiFactory({ + implements: oauthRequestApiRef, + deps: {}, + factory: () => new OAuthRequestManager(), +}); diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 5a28a43e8b..b174a0447f 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -30,6 +30,7 @@ import { ApiTestRegistry, ApiHolder, AlertDisplay, + OAuthRequestDialog, } from '@backstage/core'; import * as defaultApiFactories from './apiFactories'; @@ -90,6 +91,7 @@ class DevAppBuilder { return ( + {this.rootChildren} From 53ec396aebce6c4bcc53e10308f9b1e617c5a97c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 09:34:15 +0200 Subject: [PATCH 50/97] build(deps): bump clsx from 1.1.0 to 1.1.1 (#1084) Bumps [clsx](https://github.com/lukeed/clsx) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/lukeed/clsx/releases) - [Commits](https://github.com/lukeed/clsx/compare/v1.1.0...v1.1.1) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 77528ae6f7..54d9ec857f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6871,9 +6871,9 @@ clone@^1.0.2: integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702" - integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA== + version "1.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== cmd-shim@^3.0.0, cmd-shim@^3.0.3: version "3.0.3" From cc44775ecdb573a426c988bb6dba7b9d4e63967f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 09:36:52 +0200 Subject: [PATCH 51/97] build(deps-dev): bump cypress from 4.6.0 to 4.7.0 (#1086) Bumps [cypress](https://github.com/cypress-io/cypress) from 4.6.0 to 4.7.0. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Commits](https://github.com/cypress-io/cypress/compare/v4.6.0...v4.7.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 54d9ec857f..056b6dd57b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7866,9 +7866,9 @@ cyclist@^1.0.1: integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= cypress@*, cypress@^4.2.0: - version "4.6.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-4.6.0.tgz#ac76786500580df1347a0a50be63e5c59ffbef59" - integrity sha512-vIPXAceRP+Nxvnm/O9ruY9EQaRGmVVybtk9F1sfC9mH3067YbitrdBTynaaLuHFj90p9e0U2ZCV7OkX4x4V/Wg== + version "4.7.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-4.7.0.tgz#3ea29bddaf9a1faeaa5b8d54b60a84ed1cafa83d" + integrity sha512-Vav6wUFhPRlImIND/2lOQlUnAWzgCC/iXyJlJjX9nJOJul5LC1vUpf/m8Oiae870PFPyT0ZLLwPHKTXZNdXmHw== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/request" "2.88.5" From 0f41ac4bf7667854f4bd0c2b45b1e71e329b80d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 1 Jun 2020 09:01:32 +0200 Subject: [PATCH 52/97] Add the MockedMemberFunctions helper --- packages/backend-common/src/index.ts | 1 + .../src/testing/MockedMemberFunctions.ts | 35 +++++++++++++++++++ packages/backend-common/src/testing/index.ts | 17 +++++++++ .../catalog/DatabaseEntitiesCatalog.test.ts | 35 ++++++++++++------- 4 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 packages/backend-common/src/testing/MockedMemberFunctions.ts create mode 100644 packages/backend-common/src/testing/index.ts diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index b2c38ab506..4bc60f557f 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -17,3 +17,4 @@ export * from './errors'; export * from './logging'; export * from './middleware'; +export * from './testing'; diff --git a/packages/backend-common/src/testing/MockedMemberFunctions.ts b/packages/backend-common/src/testing/MockedMemberFunctions.ts new file mode 100644 index 0000000000..9b35908bff --- /dev/null +++ b/packages/backend-common/src/testing/MockedMemberFunctions.ts @@ -0,0 +1,35 @@ +/* + * 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. + */ + +/** + * For any type T, generate a new type that is identical but also has the + * jest.fn signature on all member functions. + * + * When writing tests against a type, you sometimes end up in a situation where + * you need to write expect(x.y as jest.Mock).toHaveBeenCalled... because the + * x.y member was considered to be the actual function type. You could also + * change your test to instead create a "raw" object { y: jest.fn() } but then + * you lose type safety when doing x.y.mockReturnValue(...). So you start + * trying to do { y: jest.fn() as X['y'] } as X or similar trickery. + * + * This type lets you say const x: MockedMemberFunctions = { y: jest.fn() } + * and keep all the type safety at every step. + */ +export type MockedMemberFunctions = { + [K in keyof T]: T[K] extends (...args: infer A) => infer B + ? T[K] & jest.Mock + : T[K]; +}; diff --git a/packages/backend-common/src/testing/index.ts b/packages/backend-common/src/testing/index.ts new file mode 100644 index 0000000000..c12297465b --- /dev/null +++ b/packages/backend-common/src/testing/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export type { MockedMemberFunctions } from './MockedMemberFunctions'; diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts index d897584c95..c5101dff7a 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -14,20 +14,31 @@ * limitations under the License. */ +import type { MockedMemberFunctions } from '@backstage/backend-common'; import type { Entity, EntityPolicy } from '@backstage/catalog-model'; import type { Database } from '../database'; import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; describe('DatabaseEntitiesCatalog', () => { - let db: Database; + let db: MockedMemberFunctions; let policy: EntityPolicy; beforeEach(() => { - // Since the database has a large API surface, we just leave it empty and - // let the tests insert whatever methods they need to call - db = ({ - transaction: jest.fn(async f => f('mock_tx')), - } as unknown) as Database; + db = { + transaction: jest.fn(), + addEntity: jest.fn(), + updateEntity: jest.fn(), + entities: jest.fn(), + entity: jest.fn(), + removeEntity: jest.fn(), + addLocation: jest.fn(), + removeLocation: jest.fn(), + location: jest.fn(), + locations: jest.fn(), + locationHistory: jest.fn(), + addLocationUpdateLogEvent: jest.fn(), + }; + db.transaction.mockImplementation(async f => f('tx')); policy = { enforce: jest.fn(async x => x) }; }); @@ -42,8 +53,8 @@ describe('DatabaseEntitiesCatalog', () => { }, }; - db.entities = jest.fn().mockResolvedValue([]); - db.addEntity = jest.fn().mockResolvedValue({ entity }); + db.entities.mockResolvedValue([]); + db.addEntity.mockResolvedValue({ entity }); const catalog = new DatabaseEntitiesCatalog(db, policy); const result = await catalog.addOrUpdateEntity(entity); @@ -65,8 +76,8 @@ describe('DatabaseEntitiesCatalog', () => { }, }; - db.entities = jest.fn().mockResolvedValue([]); - db.updateEntity = jest.fn().mockResolvedValue({ entity }); + db.entities.mockResolvedValue([]); + db.updateEntity.mockResolvedValue({ entity }); const catalog = new DatabaseEntitiesCatalog(db, policy); const result = await catalog.addOrUpdateEntity(entity); @@ -95,8 +106,8 @@ describe('DatabaseEntitiesCatalog', () => { }, }; - db.entities = jest.fn().mockResolvedValue([{ entity: existing }]); - db.updateEntity = jest.fn().mockResolvedValue({ entity: added }); + db.entities.mockResolvedValue([{ entity: existing }]); + db.updateEntity.mockResolvedValue({ entity: added }); const catalog = new DatabaseEntitiesCatalog(db, policy); const result = await catalog.addOrUpdateEntity(added); From 15a4d9357631c026e926f4034fc90e9311b3e739 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 09:45:15 +0200 Subject: [PATCH 53/97] build(deps-dev): bump start-server-and-test from 1.10.11 to 1.11.0 (#1085) Bumps [start-server-and-test](https://github.com/bahmutov/start-server-and-test) from 1.10.11 to 1.11.0. - [Release notes](https://github.com/bahmutov/start-server-and-test/releases) - [Commits](https://github.com/bahmutov/start-server-and-test/compare/v1.10.11...v1.11.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 056b6dd57b..b115b0bf9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19328,9 +19328,9 @@ stacktrace-js@^2.0.0: stacktrace-gps "^3.0.4" start-server-and-test@^1.10.11: - version "1.10.11" - resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.10.11.tgz#24290ee8a5ed15f4a34e9bb45a5d6ff93c93c83e" - integrity sha512-CZilaj293uQWdD4vgOxTOuzlCWxOyBm6bzmH1r6OGLG/q5zcBmGYevLfOimkg0kSn9jLHwYSXLuoKG/DDQJhww== + version "1.11.0" + resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.11.0.tgz#1b1a83d062b0028ee6e296bb4e0231f2d8b2f4af" + integrity sha512-FhkJFYL/lvbd0tKWvbxWNWjtFtq3Zpa09QDjA8EUH88AsgNL4hkAAKYNmbac+fFM8/GIZoJ1Mj4mm3SMI0X1bA== dependencies: bluebird "3.7.2" check-more-types "2.24.0" From 63221f6fc5a45f7f7e07022b950cd963b92e2e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Mon, 1 Jun 2020 10:58:23 +0200 Subject: [PATCH 54/97] Fix incorrect markdown links (#1088) --- packages/catalog-model/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/catalog-model/README.md b/packages/catalog-model/README.md index 755b9ee63c..6dab6e7cae 100644 --- a/packages/catalog-model/README.md +++ b/packages/catalog-model/README.md @@ -7,6 +7,6 @@ as well as by others that want to consume catalog data. ## Links -- (Default frontend part of the catalog)[https://github.com/spotify/backstage/tree/master/plugins/catalog] -- (Default backend part of the catalog)[https://github.com/spotify/backstage/tree/master/plugins/catalog-backend] -- (The Backstage homepage)[https://backstage.io] +- [Default frontend part of the catalog](https://github.com/spotify/backstage/tree/master/plugins/catalog) +- [Default backend part of the catalog](https://github.com/spotify/backstage/tree/master/plugins/catalog-backend) +- [The Backstage homepage](https://backstage.io) From 438bdba2cf09dacd9a95bc0a82597b5e0841a7ac Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Mon, 1 Jun 2020 12:07:58 +0200 Subject: [PATCH 55/97] Use StaticAuthSessionManager for Github --- .../implementations/auth/github/GithubAuth.ts | 27 +++++-------- .../src/lib/AuthSessionManager/index.ts | 1 + .../src/providers/github/provider.ts | 39 ++++--------------- 3 files changed, 18 insertions(+), 49 deletions(-) diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index d20eaa54cb..d75bceef97 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -20,7 +20,7 @@ import { GithubSession } from './types'; import { OAuthApi, AccessTokenOptions } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; -import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; +import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth @@ -63,20 +63,16 @@ class GithubAuth implements OAuthApi { sessionTransform(res: GithubAuthResponse): GithubSession { return { accessToken: res.accessToken, - scopes: GithubAuth.normalizeScopes(res.scope), + scopes: GithubAuth.normalizeScope(res.scope), expiresAt: new Date(Date.now() + res.expiresInSeconds * 1000), }; }, }); - const sessionManager = new RefreshingAuthSessionManager({ + const sessionManager = new StaticAuthSessionManager({ connector, defaultScopes: new Set(['user']), sessionScopes: session => session.scopes, - sessionShouldRefresh: session => { - const expiresInSec = (session.expiresAt.getTime() - Date.now()) / 1000; - return expiresInSec < 60 * 5; - }, }); return new GithubAuth(sessionManager); @@ -84,11 +80,8 @@ class GithubAuth implements OAuthApi { constructor(private readonly sessionManager: SessionManager) {} - async getAccessToken( - scope?: string | string[], - options?: AccessTokenOptions, - ) { - const normalizedScopes = GithubAuth.normalizeScopes(scope); + async getAccessToken(scope?: string, options?: AccessTokenOptions) { + const normalizedScopes = GithubAuth.normalizeScope(scope); const session = await this.sessionManager.getSession({ ...options, scopes: normalizedScopes, @@ -103,14 +96,14 @@ class GithubAuth implements OAuthApi { await this.sessionManager.removeSession(); } - static normalizeScopes(scopes?: string | string[]): Set { - if (!scopes) { + static normalizeScope(scope?: string): Set { + if (!scope) { return new Set(); } - const scopeList = Array.isArray(scopes) - ? scopes - : scopes.split(/[\s]/).filter(Boolean); + const scopeList = Array.isArray(scope) + ? scope + : scope.split(/[\s|,]/).filter(Boolean); return new Set(scopeList); } diff --git a/packages/core-api/src/lib/AuthSessionManager/index.ts b/packages/core-api/src/lib/AuthSessionManager/index.ts index 426c514646..16a8d3c378 100644 --- a/packages/core-api/src/lib/AuthSessionManager/index.ts +++ b/packages/core-api/src/lib/AuthSessionManager/index.ts @@ -15,4 +15,5 @@ */ export { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager'; +export { StaticAuthSessionManager } from './StaticAuthSessionManager'; export * from './types'; diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 1e8022e1cc..4445e98451 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -19,7 +19,6 @@ import { Strategy as GithubStrategy } from 'passport-github2'; import { executeFrameHandlerStrategy, executeRedirectStrategy, - executeRefreshTokenStrategy, } from '../PassportStrategyHelper'; import { OAuthProviderHandlers, @@ -37,23 +36,13 @@ export class GithubAuthProvider implements OAuthProviderHandlers { this.providerConfig = providerConfig; this._strategy = new GithubStrategy( { ...this.providerConfig.options }, - ( - accessToken: any, - refreshToken: any, - params: any, - profile: any, - done: any, - ) => { - done( - undefined, - { - profile, - accessToken, - scope: 'user', // params.scope is an empty string here for some reason, so hardcoding for now - expiresInSeconds: params.expires_in, - }, - { refreshToken }, - ); + (accessToken: any, _: any, params: any, profile: any, done: any) => { + done(undefined, { + profile, + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }); }, ); } @@ -67,18 +56,4 @@ export class GithubAuthProvider implements OAuthProviderHandlers { ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { return await executeFrameHandlerStrategy(req, this._strategy); } - - async refresh(refreshToken: string, scope: string): Promise { - const { accessToken, params } = await executeRefreshTokenStrategy( - this._strategy, - refreshToken, - scope, - ); - - return { - accessToken, - expiresInSeconds: params.expires_in, - scope: params.scope, - }; - } } From 86930c9dd2061381567c5b14564ae4e75ba6bb80 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Mon, 1 Jun 2020 12:09:05 +0200 Subject: [PATCH 56/97] Support providers without refresh tokens --- .../src/providers/OAuthProvider.ts | 36 +++++++++++++------ plugins/auth-backend/src/providers/config.ts | 1 + .../auth-backend/src/providers/factories.ts | 20 +++++++++-- plugins/auth-backend/src/providers/index.ts | 6 +--- plugins/auth-backend/src/providers/types.ts | 3 +- 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/plugins/auth-backend/src/providers/OAuthProvider.ts b/plugins/auth-backend/src/providers/OAuthProvider.ts index 81f5ed25a6..30fd60f51f 100644 --- a/plugins/auth-backend/src/providers/OAuthProvider.ts +++ b/plugins/auth-backend/src/providers/OAuthProvider.ts @@ -89,9 +89,15 @@ export const removeRefreshTokenCookie = ( export class OAuthProvider implements AuthProviderRouteHandlers { private readonly provider: string; private readonly providerHandlers: OAuthProviderHandlers; - constructor(providerHandlers: OAuthProviderHandlers, provider: string) { + private readonly disableRefresh: boolean; + constructor( + providerHandlers: OAuthProviderHandlers, + provider: string, + disableRefresh?: boolean, + ) { this.provider = provider; this.providerHandlers = providerHandlers; + this.disableRefresh = disableRefresh ?? false; } async start(req: express.Request, res: express.Response): Promise { @@ -129,14 +135,16 @@ export class OAuthProvider implements AuthProviderRouteHandlers { const { user, info } = await this.providerHandlers.handler(req); - // throw error if missing refresh token - const { refreshToken } = info; - if (!refreshToken) { - throw new Error('Missing refresh token'); - } + if (!this.disableRefresh) { + // throw error if missing refresh token + const { refreshToken } = info; + if (!refreshToken) { + throw new Error('Missing refresh token'); + } - // set new refresh token - setRefreshTokenCookie(res, this.provider, refreshToken); + // set new refresh token + setRefreshTokenCookie(res, this.provider, refreshToken); + } // post message back to popup if successful return postMessageResponse(res, { @@ -160,8 +168,10 @@ export class OAuthProvider implements AuthProviderRouteHandlers { return res.status(401).send('Invalid X-Requested-With header'); } - // remove refresh token cookie before logout - removeRefreshTokenCookie(res, this.provider); + if (!this.disableRefresh) { + // remove refresh token cookie before logout + removeRefreshTokenCookie(res, this.provider); + } return res.send('logout!'); } @@ -170,6 +180,12 @@ export class OAuthProvider implements AuthProviderRouteHandlers { return res.status(401).send('Invalid X-Requested-With header'); } + if (!this.providerHandlers.refresh || this.disableRefresh) { + return res.send( + `Refresh token not supported for provider: ${this.provider}`, + ); + } + try { const refreshToken = req.cookies[`${this.provider}-refresh-token`]; diff --git a/plugins/auth-backend/src/providers/config.ts b/plugins/auth-backend/src/providers/config.ts index dad87e0448..8d04dc5a1f 100644 --- a/plugins/auth-backend/src/providers/config.ts +++ b/plugins/auth-backend/src/providers/config.ts @@ -30,5 +30,6 @@ export const providers = [ clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!, callbackURL: 'http://localhost:7000/auth/github/handler/frame', }, + disableRefresh: true, }, ]; diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 10e4153714..0c9f99e878 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -14,9 +14,14 @@ * limitations under the License. */ -import { AuthProviderFactories, AuthProviderFactory } from './types'; +import { + AuthProviderFactories, + AuthProviderRouteHandlers, + AuthProviderConfig, +} from './types'; import { GoogleAuthProvider } from './google'; import { GithubAuthProvider } from './github'; +import { OAuthProvider } from './OAuthProvider'; export class ProviderFactories { private static readonly providerFactories: AuthProviderFactories = { @@ -24,13 +29,22 @@ export class ProviderFactories { github: GithubAuthProvider, }; - public static getProviderFactory(providerId: string): AuthProviderFactory { + public static getProviderFactory( + config: AuthProviderConfig, + ): AuthProviderRouteHandlers { + const providerId = config.provider; const ProviderImpl = ProviderFactories.providerFactories[providerId]; if (!ProviderImpl) { throw Error( `Provider Implementation missing for : ${providerId} auth provider`, ); } - return ProviderImpl; + const providerInstance = new ProviderImpl(config); + const oauthProvider = new OAuthProvider( + providerInstance, + providerId, + config.disableRefresh, + ); + return oauthProvider; } } diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 59dd116fa6..cfbabbbad1 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -17,7 +17,6 @@ import Router from 'express-promise-router'; import { AuthProviderRouteHandlers, AuthProviderConfig } from './types'; import { ProviderFactories } from './factories'; -import { OAuthProvider } from './OAuthProvider'; export const defaultRouter = (provider: AuthProviderRouteHandlers) => { const router = Router(); @@ -32,10 +31,7 @@ export const defaultRouter = (provider: AuthProviderRouteHandlers) => { export const makeProvider = (config: AuthProviderConfig) => { const providerId = config.provider; - const ProviderImpl = ProviderFactories.getProviderFactory(providerId); - const providerInstance = new ProviderImpl(config); - - const oauthProvider = new OAuthProvider(providerInstance, providerId); + const oauthProvider = ProviderFactories.getProviderFactory(config); const providerRouter = defaultRouter(oauthProvider); return { providerId, providerRouter }; }; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index dcbe6ad1de..dc83c19dd0 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -20,12 +20,13 @@ import passport from 'passport'; export type AuthProviderConfig = { provider: string; options: any; + disableRefresh?: boolean; }; export interface OAuthProviderHandlers { start(req: express.Request, options: any): Promise; handler(req: express.Request): Promise; - refresh(refreshToken: string, scope: string): Promise; + refresh?(refreshToken: string, scope: string): Promise; logout?(): Promise; } From 25a4d6ed6b62db15e8ee80ffb3cd3d152fec7c9d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 1 Jun 2020 12:07:47 +0200 Subject: [PATCH 57/97] scripts: added check-type-dependencies --- scripts/.eslintrc.js | 6 + scripts/check-type-dependencies.js | 194 +++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 scripts/.eslintrc.js create mode 100755 scripts/check-type-dependencies.js diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js new file mode 100644 index 0000000000..106fa4246a --- /dev/null +++ b/scripts/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], + rules: { + 'no-console': 0, + }, +}; diff --git a/scripts/check-type-dependencies.js b/scripts/check-type-dependencies.js new file mode 100755 index 0000000000..5b7daa13d2 --- /dev/null +++ b/scripts/check-type-dependencies.js @@ -0,0 +1,194 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const fs = require('fs'); +const { resolve: resolvePath } = require('path'); +// Cba polluting root package.json, we'll have this +// eslint-disable-next-line import/no-extraneous-dependencies +const chalk = require('chalk'); + +async function main() { + // This is from lerna, and cba polluting root package.json + // eslint-disable-next-line import/no-extraneous-dependencies + const LernaProject = require('@lerna/project'); + const project = new LernaProject(resolvePath('.')); + const packages = await project.getPackages(); + + let hadErrors = false; + + for (const pkg of packages) { + if (!shouldCheckTypes(pkg)) { + continue; + } + const { errors } = await checkTypes(pkg); + if (errors.length) { + hadErrors = true; + console.error( + `Incorrect type dependencies in ${chalk.yellow(pkg.name)}:`, + ); + for (const error of errors) { + if (error.name === 'WrongDepError') { + console.error( + ` Move from ${chalk.red(error.from)} to ${chalk.green( + error.to, + )}: ${chalk.cyan(error.dep)}`, + ); + } else if (error.name === 'MissingDepError') { + console.error( + ` Missing a type dependency: ${chalk.cyan(error.dep)}`, + ); + } else { + console.error(` Unknown error, ${chalk.red(error)}`); + } + } + } + } + + if (hadErrors) { + console.error(); + console.error( + chalk.red('At least one package had incorrect type dependencies'), + ); + + process.exit(2); + } +} + +function shouldCheckTypes(pkg) { + return !pkg.private && pkg.get('types'); +} + +/** + * Scan index.d.ts for imports and return errors for any dependency that's + * missing or incorrect in package.json + */ +function checkTypes(pkg) { + const typeDecl = fs.readFileSync( + resolvePath(pkg.location, 'dist/index.d.ts'), + 'utf8', + ); + const deps = (typeDecl.match(/from '.*'/g) || []) + .map(match => match.replace(/from '(.*)'/, '$1')) + .filter(n => !n.startsWith('.')); + + const errors = []; + const typeDeps = []; + for (const dep of deps) { + try { + const typeDep = findTypesPackage(dep, pkg); + if (typeDep) { + typeDeps.push(typeDep); + } + } catch (error) { + errors.push(error); + } + } + + errors.push(...findTypeDepErrors(typeDeps, pkg)); + + return { errors }; +} + +/** + * Find the package used for types. This assumes that types are working is a package + * can be resolved, it doesn't do any checking of presence of types inside the dep. + */ +function findTypesPackage(dep, pkg) { + try { + require.resolve(`@types/${dep}/package.json`, { paths: [pkg.location] }); + return `@types/${dep}`; + } catch { + try { + require.resolve(dep, { paths: [pkg.location] }); + return undefined; + } catch { + try { + // Some type-only modules don't have a working main field, so try resolving package.json too + require.resolve(`${dep}/package.json`, { paths: [pkg.location] }); + return undefined; + } catch { + try { + // Finally check if it's just a .d.ts file + require.resolve(`${dep}.d.ts`, { paths: [pkg.location] }); + return undefined; + } catch { + throw mkErr('MissingDepError', `No types for ${dep}`, { dep }); + } + } + } + } +} + +/** + * Figures out what type dependencies are missing, or should be moved between dep types + */ +function findTypeDepErrors(typeDeps, pkg) { + const devDeps = mkTypeDepSet(pkg.get('devDependencies')); + const deps = mkTypeDepSet(pkg.get('dependencies')); + + const errors = []; + for (const typeDep of typeDeps) { + if (!deps.has(typeDep)) { + if (devDeps.has(typeDep)) { + errors.push( + mkErr('WrongDepError', `Should be dep ${typeDep}`, { + dep: typeDep, + from: 'devDependencies', + to: 'dependencies', + }), + ); + } else { + errors.push( + mkErr('MissingDepError', `No types for ${typeDep}`, { + dep: typeDep, + }), + ); + } + } else { + deps.delete(typeDep); + } + } + + for (const dep of deps) { + errors.push( + mkErr('WrongDepError', `Should be dev dep ${dep}`, { + dep, + from: 'dependencies', + to: 'devDependencies', + }), + ); + } + + return errors; +} + +function mkTypeDepSet(deps) { + const typeDeps = Object.keys(deps || {}).filter(n => n.startsWith('@types/')); + return new Set(typeDeps); +} + +function mkErr(name, msg, extra) { + const error = new Error(msg); + error.name = name; + Object.assign(error, extra); + return error; +} + +main().catch(error => { + console.error(error.stack || error); + process.exit(1); +}); From c6abe12e45507621202de1123caa9b84eec41d16 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 1 Jun 2020 12:27:29 +0200 Subject: [PATCH 58/97] packages,plugins: fix incorrect type dependencies --- packages/core-api/package.json | 7 ++++--- packages/core-api/src/routing/index.ts | 1 + packages/core/package.json | 13 +++++++------ packages/dev-utils/package.json | 7 +++++-- packages/test-utils-core/package.json | 7 +++++-- packages/test-utils/package.json | 7 +++++-- plugins/tech-radar/package.json | 1 + yarn.lock | 2 +- 8 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 73e7a18d33..a8c2b939ac 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -31,9 +31,7 @@ "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", - "@types/jest": "^25.2.2", - "@types/node": "^12.0.0", - "@types/zen-observable": "^0.8.0", + "@types/react": "^16.9", "prop-types": "^15.7.2", "react": "^16.12.0", "react-router-dom": "^5.2.0", @@ -46,6 +44,9 @@ "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0", + "@types/zen-observable": "^0.8.0", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index 98e4f46d98..67d4c82167 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -16,3 +16,4 @@ export * from './types'; export { createRouteRef } from './RouteRef'; +export type { MutableRouteRef } from './RouteRef'; diff --git a/packages/core/package.json b/packages/core/package.json index a89ffc0b92..452633a4e5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,13 +33,8 @@ "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "@types/classnames": "^2.2.9", - "@types/google-protobuf": "^3.7.2", - "@types/jest": "^25.2.2", - "@types/node": "^12.0.0", - "@types/react-helmet": "^5.0.15", + "@types/react": "^16.9", "@types/react-sparklines": "^1.7.0", - "@types/zen-observable": "^0.8.0", "classnames": "^2.2.6", "clsx": "^1.1.0", "lodash": "^4.17.15", @@ -61,6 +56,12 @@ "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", + "@types/classnames": "^2.2.9", + "@types/google-protobuf": "^3.7.2", + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0", + "@types/react-helmet": "^5.0.15", + "@types/zen-observable": "^0.8.0", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 8c4b960ebb..fc77298371 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -37,14 +37,17 @@ "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", - "@types/jest": "^25.2.2", - "@types/node": "^12.0.0", + "@types/react": "^16.9", "react": "^16.12.0", "react-dom": "^16.12.0", "react-hot-loader": "^4.12.21", "react-router": "^5.2.0", "react-router-dom": "^5.2.0" }, + "devDependencies": { + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0" + }, "files": [ "dist/**/*.{js,d.ts}" ] diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index ad4bbc95c9..f629877a65 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -30,11 +30,14 @@ "dependencies": { "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", - "@types/jest": "^25.2.2", - "@types/node": "^12.0.0", + "@types/react": "^16.9", "react": "^16.12.0", "react-dom": "^16.12.0" }, + "devDependencies": { + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0" + }, "files": [ "dist/**/*.{js,d.ts}" ] diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index fd893f8c8c..4bb7305671 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -35,13 +35,16 @@ "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", - "@types/jest": "^25.2.2", - "@types/node": "^12.0.0", + "@types/react": "^16.9", "react": "^16.12.0", "react-dom": "^16.12.0", "react-router": "^5.2.0", "react-router-dom": "^5.2.0" }, + "devDependencies": { + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0" + }, "files": [ "dist/**/*.{js,d.ts}" ] diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 3a9c75a3f7..22568f8b97 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -26,6 +26,7 @@ "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "@types/react": "^16.9", "color": "^3.1.2", "d3-force": "^2.0.1", "prop-types": "^15.7.2", diff --git a/yarn.lock b/yarn.lock index b115b0bf9f..80e3916f53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4417,7 +4417,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.8.19": +"@types/react@*", "@types/react@^16.8.19", "@types/react@^16.9": version "16.9.25" resolved "https://registry.npmjs.org/@types/react/-/react-16.9.25.tgz#6ae2159b40138c792058a23c3c04fd3db49e929e" integrity sha512-Dlj2V72cfYLPNscIG3/SMUOzhzj7GK3bpSrfefwt2YT9GLynvLCCZjbhyF6VsT0q0+aRACRX03TDJGb7cA0cqg== From 54ca975bc1aacfad23ad7aa92197d98d74d8a257 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 1 Jun 2020 12:35:40 +0200 Subject: [PATCH 59/97] package.json: add lint:type-deps script --- docs/getting-started/development-environment.md | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index 4dd2f7c9c7..489116fcc8 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -44,6 +44,7 @@ yarn build # Build published versions of packages, depends on tsc yarn lint # lint packages that have changed since later commit on origin/master yarn lint:all # lint all packages +yarn lint:type-deps # verify that @types/* dependencies are placed correctly in packages yarn test # test packages that have changed since later commit on origin/master yarn test:all # test all packages diff --git a/package.json b/package.json index de924894c7..437ce36c4b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test:all": "lerna run test -- --coverage", "lint": "lerna run lint --since origin/master --", "lint:all": "lerna run lint --", + "lint:type-deps": "node scripts/check-type-dependencies.js", "docker-build": "yarn bundle && docker build . -t spotify/backstage", "create-plugin": "backstage-cli create-plugin", "remove-plugin": "backstage-cli remove-plugin", From 7c820941a6769c2fc9663c2bb419bdb530f7f0d5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 1 Jun 2020 12:35:47 +0200 Subject: [PATCH 60/97] workflows: verify type dependencies --- .github/workflows/frontend.yml | 3 +++ .github/workflows/master.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index d916141588..55a594d55e 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -71,6 +71,9 @@ jobs: if: ${{ steps.yarn-lock.outcome == 'failure' }} run: yarn lerna -- run build + - name: verify type dependencies + run: yarn lint:type-deps + - name: test changed packages if: ${{ steps.yarn-lock.outcome == 'success' }} run: yarn lerna -- run test --since origin/master -- --coverage diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index a41f25c262..14619305cd 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -60,6 +60,9 @@ jobs: - name: build run: yarn build + - name: verify type dependencies + run: yarn lint:type-deps + - name: test run: yarn lerna -- run test -- --coverage From e32f5a49ffd7159ac841aee0c32f1dc91d1b05d1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 19:04:55 +0200 Subject: [PATCH 61/97] packages/test-utils: do app wrapping with actual app + remove wrapInThemedTestApp --- .../CodeSnippet/CodeSnippet.test.tsx | 10 ++- .../CopyTextButton/CopyTextButton.test.tsx | 6 +- .../DismissableBanner.test.js | 4 +- .../HorizontalScrollGrid.test.jsx | 6 +- .../components/Lifecycle/Lifecycle.test.jsx | 12 ++-- .../ProgressBars/CircleProgress.test.jsx | 14 ++--- .../ProgressBars/ProgressCard.test.jsx | 16 ++--- .../components/TrendLine/TrendLine.test.tsx | 12 ++-- .../WarningPanel/WarningPanel.test.tsx | 8 +-- .../ContentHeader/ContentHeader.test.tsx | 10 ++- .../src/layout/ErrorPage/ErrorPage.test.tsx | 6 +- .../core/src/layout/Header/Header.test.tsx | 12 ++-- .../HeaderActionMenu.test.tsx | 12 ++-- .../layout/HeaderLabel/HeaderLabel.test.tsx | 14 ++--- packages/test-utils/package.json | 1 + .../src/testUtils/appWrappers.test.tsx | 2 +- .../test-utils/src/testUtils/appWrappers.tsx | 63 ++++++++++++++----- .../CatalogFilter/CatalogFilter.test.tsx | 10 +-- .../CatalogPage/CatalogPage.test.tsx | 4 +- .../CatalogTable/CatalogTable.test.tsx | 8 +-- .../ComponentPage/ComponentPage.test.tsx | 4 +- .../src/components/ExploreCard.test.js | 20 +++--- .../AuditList/AuditListTable.test.tsx | 35 ++++------- .../src/components/AuditList/index.test.tsx | 37 +++++------ .../src/components/AuditView/index.test.tsx | 20 +++--- .../src/components/CreateAudit/index.test.tsx | 28 ++++----- .../src/components/Intro/index.test.tsx | 10 +-- 27 files changed, 185 insertions(+), 199 deletions(-) diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx index 0d0c34a3e6..c019b316fd 100644 --- a/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx +++ b/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import CodeSnippet from './CodeSnippet'; @@ -33,16 +33,14 @@ const minProps = { describe('', () => { it('renders text without exploding', () => { - const { getByText } = render( - wrapInThemedTestApp(), - ); + const { getByText } = render(wrapInTestApp()); expect(getByText(/"Hello"/)).toBeInTheDocument(); expect(getByText(/"World"/)).toBeInTheDocument(); }); it('renders without line numbers', () => { const { queryByText } = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(queryByText('1')).not.toBeInTheDocument(); expect(queryByText('2')).not.toBeInTheDocument(); @@ -51,7 +49,7 @@ describe('', () => { it('renders with line numbers', () => { const { queryByText } = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(queryByText(/1/)).toBeInTheDocument(); expect(queryByText(/2/)).toBeInTheDocument(); diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx index e0f7271014..dd83cd318d 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import CopyTextButton from './CopyTextButton'; import { ApiRegistry, @@ -57,7 +57,7 @@ const apiRegistry = ApiRegistry.from([ describe('', () => { it('renders without exploding', () => { const { getByDisplayValue } = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -69,7 +69,7 @@ describe('', () => { it('displays tooltip on click', async () => { document.execCommand = jest.fn(); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.test.js b/packages/core/src/components/DismissableBanner/DismissableBanner.test.js index 485b6226d2..8981acf8af 100644 --- a/packages/core/src/components/DismissableBanner/DismissableBanner.test.js +++ b/packages/core/src/components/DismissableBanner/DismissableBanner.test.js @@ -16,7 +16,7 @@ import React from 'react'; // import { fireEvent, waitForElementToBeRemoved } from '@testing-library/react'; -import { renderWithEffects, wrapInThemedTestApp } from '@backstage/test-utils'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; // import { createSetting } from 'shared/apis/settings'; import DismissableBanner from './DismissableBanner'; @@ -30,7 +30,7 @@ describe('', () => { */ const rendered = await renderWithEffects( - wrapInThemedTestApp( + wrapInTestApp( ', () => { it('renders without exploding', () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( item1 item2 @@ -69,7 +69,7 @@ describe('', () => { }; const rendered = await renderWithEffects( - wrapInThemedTestApp( + wrapInTestApp( item1 diff --git a/packages/core/src/components/Lifecycle/Lifecycle.test.jsx b/packages/core/src/components/Lifecycle/Lifecycle.test.jsx index db0d2736ed..ac3823d85c 100644 --- a/packages/core/src/components/Lifecycle/Lifecycle.test.jsx +++ b/packages/core/src/components/Lifecycle/Lifecycle.test.jsx @@ -16,29 +16,27 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import { Lifecycle } from './Lifecycle'; describe('', () => { it('renders Alpha with shorthand', async () => { - const { getByText } = render( - wrapInThemedTestApp(), - ); + const { getByText } = render(wrapInTestApp()); expect(getByText('α')).toBeInTheDocument(); }); it('renders Alpha without shorthand', async () => { - const { getByText } = render(wrapInThemedTestApp()); + const { getByText } = render(wrapInTestApp()); expect(getByText('Alpha')).toBeInTheDocument(); }); it('renders Beta with shorthand', async () => { - const { getByText } = render(wrapInThemedTestApp()); + const { getByText } = render(wrapInTestApp()); expect(getByText('β')).toBeInTheDocument(); }); it('renders Beta without shorthand', async () => { - const { getByText } = render(wrapInThemedTestApp()); + const { getByText } = render(wrapInTestApp()); expect(getByText('Beta')).toBeInTheDocument(); }); }); diff --git a/packages/core/src/components/ProgressBars/CircleProgress.test.jsx b/packages/core/src/components/ProgressBars/CircleProgress.test.jsx index 4975e00cb8..b42559b7b7 100644 --- a/packages/core/src/components/ProgressBars/CircleProgress.test.jsx +++ b/packages/core/src/components/ProgressBars/CircleProgress.test.jsx @@ -16,37 +16,33 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import CircleProgress, { getProgressColor } from './CircleProgress'; describe('', () => { it('renders without exploding', () => { const { getByText } = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); getByText('10%'); }); it('handles fractional prop', () => { const { getByText } = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); getByText('10%'); }); it('handles max prop', () => { const { getByText } = render( - wrapInThemedTestApp( - , - ), + wrapInTestApp(), ); getByText('1%'); }); it('handles unit prop', () => { const { getByText } = render( - wrapInThemedTestApp( - , - ), + wrapInTestApp(), ); getByText('10m'); }); diff --git a/packages/core/src/components/ProgressBars/ProgressCard.test.jsx b/packages/core/src/components/ProgressBars/ProgressCard.test.jsx index 7357cab812..3e93e3f302 100644 --- a/packages/core/src/components/ProgressBars/ProgressCard.test.jsx +++ b/packages/core/src/components/ProgressBars/ProgressCard.test.jsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import ProgressCard from './ProgressCard'; @@ -24,32 +24,26 @@ const minProps = { title: 'Tingle upgrade', progress: 0.12 }; describe('', () => { it('renders without exploding', () => { - const { getByText } = render( - wrapInThemedTestApp(), - ); + const { getByText } = render(wrapInTestApp()); expect(getByText(/Tingle.*/)).toBeInTheDocument(); }); it('renders progress and title', () => { - const { getByText } = render( - wrapInThemedTestApp(), - ); + const { getByText } = render(wrapInTestApp()); expect(getByText(/Tingle.*/)).toBeInTheDocument(); expect(getByText(/12%.*/)).toBeInTheDocument(); }); it('does not render deepLink', () => { const { queryByText } = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(queryByText('View more')).not.toBeInTheDocument(); }); it('handles invalid numbers', () => { const badProps = { title: 'Tingle upgrade', progress: 'hejjo' }; - const { getByText } = render( - wrapInThemedTestApp(), - ); + const { getByText } = render(wrapInTestApp()); expect(getByText(/N\/A.*/)).toBeInTheDocument(); }); }); diff --git a/packages/core/src/components/TrendLine/TrendLine.test.tsx b/packages/core/src/components/TrendLine/TrendLine.test.tsx index 985e5d2b31..84e8413f01 100644 --- a/packages/core/src/components/TrendLine/TrendLine.test.tsx +++ b/packages/core/src/components/TrendLine/TrendLine.test.tsx @@ -17,7 +17,7 @@ /* eslint-disable jest/no-disabled-tests */ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import TrendLine from '.'; @@ -25,7 +25,7 @@ describe('TrendLine', () => { describe('when no data is present', () => { it('renders null without throwing', () => { const rendered = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(rendered.queryByTitle('sparkline')).not.toBeInTheDocument(); }); @@ -34,7 +34,7 @@ describe('TrendLine', () => { describe('when one datapoint is present', () => { it('renders as a straight line', () => { const rendered = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(rendered.getByTitle('sparkline')).toBeInTheDocument(); }); @@ -43,7 +43,7 @@ describe('TrendLine', () => { describe.skip('when the data finishes above the success threshold', () => { it('renders with the correct color', () => { const rendered = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(rendered.getByTitle('sparkline')).toBeInTheDocument(); }); @@ -52,7 +52,7 @@ describe('TrendLine', () => { describe.skip('when the data finishes within the the warning threshold', () => { it('renders with the correct color', () => { const rendered = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(rendered.getByTitle('sparkline')).toBeInTheDocument(); }); @@ -61,7 +61,7 @@ describe('TrendLine', () => { describe.skip('when the data finishes within the the error threshold', () => { it('renders with the correct color', () => { const rendered = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(rendered.getByTitle('sparkline')).toBeInTheDocument(); }); diff --git a/packages/core/src/components/WarningPanel/WarningPanel.test.tsx b/packages/core/src/components/WarningPanel/WarningPanel.test.tsx index 4094c65a1f..c4d836cfe1 100644 --- a/packages/core/src/components/WarningPanel/WarningPanel.test.tsx +++ b/packages/core/src/components/WarningPanel/WarningPanel.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import WarningPanel from './WarningPanel'; @@ -24,15 +24,13 @@ const minProps = { title: 'Mock title', message: 'Some more info' }; describe('', () => { it('renders without exploding', () => { - const { getByText } = render( - wrapInThemedTestApp(), - ); + const { getByText } = render(wrapInTestApp()); expect(getByText('Mock title')).toBeInTheDocument(); }); it('renders message and children', () => { const { getByText } = render( - wrapInThemedTestApp(children), + wrapInTestApp(children), ); expect(getByText('Some more info')).toBeInTheDocument(); expect(getByText('children')).toBeInTheDocument(); diff --git a/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx b/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx index 2f40511f35..5db676dcc5 100644 --- a/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx +++ b/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { ContentHeader } from './ContentHeader'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; jest.mock('react-helmet', () => { return { @@ -27,9 +27,7 @@ jest.mock('react-helmet', () => { describe('', () => { it('should render with title', () => { - const rendered = render( - wrapInThemedTestApp(), - ); + const rendered = render(wrapInTestApp()); rendered.getByText('Title'); }); @@ -37,14 +35,14 @@ describe('', () => { const title = 'Custom title'; const titleComponent = () =>

{title}

; const rendered = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); rendered.getByText(title); }); it('should render with description', () => { const rendered = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); rendered.getByText('description'); }); diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx index d383c3fbf0..c47ff8d49e 100644 --- a/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx +++ b/packages/core/src/layout/ErrorPage/ErrorPage.test.tsx @@ -17,14 +17,12 @@ import React from 'react'; import { render } from '@testing-library/react'; import { ErrorPage } from './ErrorPage'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; describe('', () => { it('should render with status code, status message and go back link', () => { const rendered = render( - wrapInThemedTestApp( - , - ), + wrapInTestApp(), ); rendered.getByText(/page not found/i); rendered.getByText(/404/i); diff --git a/packages/core/src/layout/Header/Header.test.tsx b/packages/core/src/layout/Header/Header.test.tsx index c1f9be6cf6..5d28c6633f 100644 --- a/packages/core/src/layout/Header/Header.test.tsx +++ b/packages/core/src/layout/Header/Header.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import { Header } from './Header'; jest.mock('react-helmet', () => { @@ -27,19 +27,19 @@ jest.mock('react-helmet', () => { describe('
', () => { it('should render with title', () => { - const rendered = render(wrapInThemedTestApp(
)); + const rendered = render(wrapInTestApp(
)); rendered.getByText('Title'); }); it('should set document title', () => { - const rendered = render(wrapInThemedTestApp(
)); + const rendered = render(wrapInTestApp(
)); rendered.getByText('Title1'); rendered.getByText('defaultTitle: Title1 | Backstage'); }); it('should override document title', () => { const rendered = render( - wrapInThemedTestApp(
), + wrapInTestApp(
), ); rendered.getByText('Title1'); rendered.getByText('defaultTitle: Title2 | Backstage'); @@ -47,14 +47,14 @@ describe('
', () => { it('should have subtitle', () => { const rendered = render( - wrapInThemedTestApp(
), + wrapInTestApp(
), ); rendered.getByText('Subtitle'); }); it('should have type rendered', () => { const rendered = render( - wrapInThemedTestApp(
), + wrapInTestApp(
), ); rendered.getByText('tool'); }); diff --git a/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx b/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx index a1b5740f88..00fa4d27ac 100644 --- a/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx +++ b/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx @@ -16,18 +16,18 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { wrapInThemedTestApp, Keyboard } from '@backstage/test-utils'; +import { wrapInTestApp, Keyboard } from '@backstage/test-utils'; import { HeaderActionMenu } from './HeaderActionMenu'; describe('', () => { it('renders without any items and without exploding', () => { - render(wrapInThemedTestApp()); + render(wrapInTestApp()); }); it('can open the menu and click menu items', () => { const onClickFunction = jest.fn(); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -49,7 +49,7 @@ describe('', () => { it('Disabled', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -66,7 +66,7 @@ describe('', () => { it('Test wrapper, and secondary label', () => { const onClickFunction = jest.fn(); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( ', () => { it('should close when hitting escape', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , ), ); diff --git a/packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx b/packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx index 11a22f9b5d..fdc6ef8c6e 100644 --- a/packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx +++ b/packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx @@ -16,39 +16,37 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import { HeaderLabel } from './HeaderLabel'; describe('', () => { it('should have a label', () => { - const rendered = render(wrapInThemedTestApp()); + const rendered = render(wrapInTestApp()); expect(rendered.getByText('Label')).toBeInTheDocument(); }); it('should say unknown', () => { - const rendered = render(wrapInThemedTestApp()); + const rendered = render(wrapInTestApp()); expect(rendered.getByText('')).toBeInTheDocument(); }); it('should say unknown when passing null as value prop', () => { const rendered = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(rendered.getByText('')).toBeInTheDocument(); }); it('should have value', () => { const rendered = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(rendered.getByText('Value')).toBeInTheDocument(); }); it('should have a link', () => { const rendered = render( - wrapInThemedTestApp( - , - ), + wrapInTestApp(), ); const anchor = rendered.container.querySelector('a') as HTMLAnchorElement; expect(rendered.getByText('Value')).toBeInTheDocument(); diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index fd893f8c8c..c35e3a622e 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/core-api": "^0.1.1-alpha.6", "@backstage/test-utils-core": "^0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index e2f1b6b7cb..e46b6a95ce 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -27,7 +27,7 @@ describe('wrapInTestApp', () => { Route 1 Route 2 , - ['/route2'], + { routeEntries: ['/route2'] }, ), ); expect(rendered.getByText('Route 2')).toBeInTheDocument(); diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 06d8781669..c5839610d3 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -15,15 +15,52 @@ */ import React, { ComponentType, ReactNode, FunctionComponent } from 'react'; -import { ThemeProvider } from '@material-ui/core'; import { MemoryRouter } from 'react-router'; import { Route } from 'react-router-dom'; import { lightTheme } from '@backstage/theme'; +import privateExports, { + defaultSystemIcons, + ApiTestRegistry, +} from '@backstage/core-api'; +const { PrivateAppImpl } = privateExports; + +const NotFoundErrorPage = () => { + throw new Error('Reached NotFound Page'); +}; + +/** + * Options to customize the behavior of the test app wrapper. + */ +type TestAppOptions = { + /** + * Initial route entries to pass along as `initialEntries` to the router. + */ + routeEntries?: string[]; +}; export function wrapInTestApp( Component: ComponentType | ReactNode, - initialRouterEntries: string[] = ['/'], + options: TestAppOptions = {}, ) { + const { routeEntries = ['/'] } = options; + + const app = new PrivateAppImpl({ + apis: new ApiTestRegistry(), + components: { + NotFoundErrorPage, + }, + icons: defaultSystemIcons, + plugins: [], + themes: [ + { + id: 'light', + theme: lightTheme, + title: 'Test App Theme', + variant: 'light', + }, + ], + }); + let Wrapper: ComponentType; if (Component instanceof Function) { Wrapper = Component; @@ -31,21 +68,13 @@ export function wrapInTestApp( Wrapper = (() => Component) as FunctionComponent; } + const AppProvider = app.getProvider(); + return ( - - - + + + + + ); } - -export function wrapInThemedTestApp( - component: ReactNode, - initialRouterEntries: string[] = ['/'], -) { - const themed = {component}; - return wrapInTestApp(themed, initialRouterEntries); -} - -export const wrapInTheme = (component: ReactNode, theme = lightTheme) => ( - {component} -); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index 762a881302..ec44f53777 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; describe('Catalog Filter', () => { @@ -26,7 +26,7 @@ describe('Catalog Filter', () => { { name: 'Test Group 2', items: [] }, ]; const { findByText } = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); for (const group of mockGroups) { @@ -52,7 +52,7 @@ describe('Catalog Filter', () => { ]; const { findByText } = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); const [group] = mockGroups; @@ -81,7 +81,7 @@ describe('Catalog Filter', () => { ]; const { findByText } = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); const [group] = mockGroups; @@ -112,7 +112,7 @@ describe('Catalog Filter', () => { const onSelectedChangeHandler = jest.fn(); const { findByText } = render( - wrapInThemedTestApp( + wrapInTestApp( {} }; @@ -30,7 +30,7 @@ describe('CatalogPage', () => { // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { const rendered = render( - wrapInTheme( + wrapInTestApp( { it('should render loading when loading prop it set to true', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , ), ); @@ -38,7 +38,7 @@ describe('CatalogTable component', () => { it('should render error message when error is passed in props', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( { it('should display component names when loading has finished and no error occurred', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( { @@ -38,7 +38,7 @@ describe('ComponentPage', () => { it('should redirect to component table page when name is not provided', async () => { const props = getTestProps(''); await render( - wrapInTheme( + wrapInTestApp( , diff --git a/plugins/explore/src/components/ExploreCard.test.js b/plugins/explore/src/components/ExploreCard.test.js index fde36b529d..45652015a5 100644 --- a/plugins/explore/src/components/ExploreCard.test.js +++ b/plugins/explore/src/components/ExploreCard.test.js @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import ExploreCard from './ExploreCard'; @@ -32,22 +32,18 @@ const minProps = { describe('', () => { it('renders without exploding', () => { - const { getByText } = render( - wrapInThemedTestApp(), - ); + const { getByText } = render(wrapInTestApp()); expect(getByText('Explore')).toBeInTheDocument(); }); it('renders props correctly', () => { - const { getByText } = render( - wrapInThemedTestApp(), - ); + const { getByText } = render(wrapInTestApp()); expect(getByText(minProps.card.title)).toBeInTheDocument(); expect(getByText(minProps.card.description)).toBeInTheDocument(); }); it('should link out', () => { - const rendered = render(wrapInThemedTestApp()); + const rendered = render(wrapInTestApp()); const anchor = rendered.container.querySelector('a'); expect(anchor.href).toBe(minProps.card.url); }); @@ -63,7 +59,7 @@ describe('', () => { }, }; const { getByText } = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(getByText('Description missing')).toBeInTheDocument(); }); @@ -78,15 +74,13 @@ describe('', () => { }, }; const { queryByText } = render( - wrapInThemedTestApp(), + wrapInTestApp(), ); expect(queryByText('GA')).not.toBeInTheDocument(); }); it('renders tags correctly', () => { - const { getByText } = render( - wrapInThemedTestApp(), - ); + const { getByText } = render(wrapInTestApp()); expect(getByText(minProps.card.tags[0])).toBeInTheDocument(); expect(getByText(minProps.card.tags[1])).toBeInTheDocument(); }); diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx index cebab93f17..039b397083 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import { ApiRegistry, ApiProvider } from '@backstage/core'; import AuditListTable from './AuditListTable'; @@ -50,12 +50,10 @@ describe('AuditListTable', () => { ); }; it('renders the link to each website', () => { - const rendered = render( - wrapInThemedTestApp(auditList(websiteListResponse)), - ); + const rendered = render(wrapInTestApp(auditList(websiteListResponse))); const link = rendered.queryByText('https://anchor.fm'); const website = websiteListResponse.items.find( - (w) => w.url === 'https://anchor.fm', + w => w.url === 'https://anchor.fm', ); if (!website) throw new Error('https://anchor.fm must be present in fixture'); @@ -67,11 +65,9 @@ describe('AuditListTable', () => { }); it('renders the dates that are available for a given row', () => { - const rendered = render( - wrapInThemedTestApp(auditList(websiteListResponse)), - ); + const rendered = render(wrapInTestApp(auditList(websiteListResponse))); const website = websiteListResponse.items.find( - (w) => w.url === 'https://anchor.fm', + w => w.url === 'https://anchor.fm', ); if (!website) throw new Error('https://anchor.fm must be present in fixture'); @@ -81,35 +77,30 @@ describe('AuditListTable', () => { }); it('renders the status for a given row', async () => { - const rendered = render( - wrapInThemedTestApp(auditList(websiteListResponse)), - ); + const rendered = render(wrapInTestApp(auditList(websiteListResponse))); const completed = await rendered.findAllByText('COMPLETED'); expect(completed).toHaveLength( - websiteListResponse.items.filter( - (w) => w.lastAudit.status === 'COMPLETED', - ).length, + websiteListResponse.items.filter(w => w.lastAudit.status === 'COMPLETED') + .length, ); const failed = await rendered.findAllByText('FAILED'); expect(failed).toHaveLength( - websiteListResponse.items.filter((w) => w.lastAudit.status === 'FAILED') + websiteListResponse.items.filter(w => w.lastAudit.status === 'FAILED') .length, ); const running = await rendered.findAllByText('FAILED'); expect(running).toHaveLength( - websiteListResponse.items.filter((w) => w.lastAudit.status === 'RUNNING') + websiteListResponse.items.filter(w => w.lastAudit.status === 'RUNNING') .length, ); }); describe('sparklines', () => { it('correctly maps the data from the website payload', () => { - const rendered = render( - wrapInThemedTestApp(auditList(websiteListResponse)), - ); + const rendered = render(wrapInTestApp(auditList(websiteListResponse))); const backstageSEO = rendered.getByTitle( 'trendline for SEO category of https://backstage.io', ); @@ -117,9 +108,7 @@ describe('AuditListTable', () => { }); it('does not break when no data is available', () => { - const rendered = render( - wrapInThemedTestApp(auditList(websiteListResponse)), - ); + const rendered = render(wrapInTestApp(auditList(websiteListResponse))); const anchorSEO = rendered.queryByTitle( 'trendline for SEO category of https://anchor.fm', ); diff --git a/plugins/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx index 9bd6e98aaa..bcd486e876 100644 --- a/plugins/lighthouse/src/components/AuditList/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx @@ -27,11 +27,10 @@ jest.mock('react-router-dom', () => { }); import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; import mockFetch from 'jest-fetch-mock'; import { render, fireEvent } from '@testing-library/react'; import { ApiRegistry, ApiProvider } from '@backstage/core'; -import { wrapInThemedTestApp, wrapInTheme } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import { lighthouseApiRef, @@ -57,7 +56,7 @@ describe('AuditList', () => { it('should render the table', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -69,7 +68,7 @@ describe('AuditList', () => { it('renders a link to create a new audit', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -87,12 +86,11 @@ describe('AuditList', () => { it('requests the correct limit and offset from the api based on the query', () => { mockFetch.mockClear(); render( - wrapInTheme( - - - - - , + wrapInTestApp( + + + , + { routeEntries: ['/lighthouse?page=2'] }, ), ); expect(mockFetch).toHaveBeenLastCalledWith( @@ -104,7 +102,7 @@ describe('AuditList', () => { describe('when only one page is needed', () => { it('hides pagination elements', () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -125,7 +123,7 @@ describe('AuditList', () => { it('shows pagination elements', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -138,12 +136,11 @@ describe('AuditList', () => { it('changes the page on click', async () => { const rendered = render( - wrapInTheme( - - - - - , + wrapInTestApp( + + + , + { routeEntries: ['/lighthouse?page=2'] }, ), ); const element = await rendered.findByLabelText(/Go to page 1/); @@ -157,7 +154,7 @@ describe('AuditList', () => { it('should render the loader', async () => { mockFetch.mockResponseOnce(() => new Promise(() => {})); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -172,7 +169,7 @@ describe('AuditList', () => { it('should render an error', async () => { mockFetch.mockRejectOnce(new Error('failed to fetch')); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , diff --git a/plugins/lighthouse/src/components/AuditView/index.test.tsx b/plugins/lighthouse/src/components/AuditView/index.test.tsx index 4e261fcf39..b2c15cd8b7 100644 --- a/plugins/lighthouse/src/components/AuditView/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.test.tsx @@ -27,7 +27,7 @@ jest.mock('react-router-dom', () => { import React from 'react'; import mockFetch from 'jest-fetch-mock'; import { render } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import { ApiRegistry, ApiProvider } from '@backstage/core'; import AuditView from '.'; @@ -56,7 +56,7 @@ describe('AuditView', () => { it('renders the iframe for the selected audit', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -72,7 +72,7 @@ describe('AuditView', () => { it('renders a link to create a new audit for this website', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -92,7 +92,7 @@ describe('AuditView', () => { describe('sidebar', () => { it('renders a list of all audits for the website', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -110,7 +110,7 @@ describe('AuditView', () => { it('sets the current audit as active', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -138,7 +138,7 @@ describe('AuditView', () => { it('navigates to the next report when an audit is clicked', async () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -160,7 +160,7 @@ describe('AuditView', () => { it('it shows the loading', async () => { mockFetch.mockImplementationOnce(() => new Promise(() => {})); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -174,7 +174,7 @@ describe('AuditView', () => { it('it shows an error', async () => { mockFetch.mockRejectOnce(new Error('failed to fetch')); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -191,7 +191,7 @@ describe('AuditView', () => { useParams.mockReturnValueOnce({ id }); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -211,7 +211,7 @@ describe('AuditView', () => { useParams.mockReturnValueOnce({ id }); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx index 79897539c4..08e8005530 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx @@ -29,14 +29,13 @@ jest.mock('react-router-dom', () => { import React from 'react'; import mockFetch from 'jest-fetch-mock'; import { wait, render, fireEvent } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; import { ApiRegistry, ApiProvider, ErrorApi, errorApiRef, } from '@backstage/core'; -import { wrapInThemedTestApp, wrapInTheme } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import { lighthouseApiRef, LighthouseRestApi, Audit } from '../../api'; import CreateAudit from '.'; @@ -62,7 +61,7 @@ describe('CreateAudit', () => { it('renders the form', () => { const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -77,16 +76,15 @@ describe('CreateAudit', () => { it('prefills the url into the form', () => { const url = 'https://spotify.com'; const rendered = render( - wrapInTheme( - + + , + { + routeEntries: [ `/lighthouse/create-audit?url=${encodeURIComponent(url)}`, - ]} - > - - - - , + ], + }, ), ); expect(rendered.getByLabelText(/URL/)).toHaveAttribute('value', url); @@ -98,7 +96,7 @@ describe('CreateAudit', () => { mockFetch.mockResponseOnce(() => new Promise(() => {})); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -121,7 +119,7 @@ describe('CreateAudit', () => { mockFetch.mockResponseOnce(JSON.stringify(createAuditResponse)); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , @@ -152,7 +150,7 @@ describe('CreateAudit', () => { mockFetch.mockRejectOnce(new Error('failed to post')); const rendered = render( - wrapInThemedTestApp( + wrapInTestApp( , diff --git a/plugins/lighthouse/src/components/Intro/index.test.tsx b/plugins/lighthouse/src/components/Intro/index.test.tsx index e64e8114d3..183008dc3c 100644 --- a/plugins/lighthouse/src/components/Intro/index.test.tsx +++ b/plugins/lighthouse/src/components/Intro/index.test.tsx @@ -18,13 +18,13 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { wrapInTestApp } from '@backstage/test-utils'; import LighthouseIntro from '.'; describe('LighthouseIntro', () => { it('renders successfully', () => { - const rendered = render(wrapInThemedTestApp()); + const rendered = render(wrapInTestApp()); expect( rendered.queryByText('Welcome to Lighthouse in Backstage!'), ).toBeInTheDocument(); @@ -35,13 +35,13 @@ describe('LighthouseIntro', () => { const secondTabRe = /you will need a running instance of/; it('selects the first text element', () => { - const rendered = render(wrapInThemedTestApp()); + const rendered = render(wrapInTestApp()); expect(rendered.queryByText(firstTabRe)).toBeInTheDocument(); expect(rendered.queryByText(secondTabRe)).not.toBeInTheDocument(); }); it('shows the other text when the tab is clicked', () => { - const rendered = render(wrapInThemedTestApp()); + const rendered = render(wrapInTestApp()); fireEvent.click(rendered.getByText('Setup')); expect(rendered.queryByText(firstTabRe)).not.toBeInTheDocument(); expect(rendered.queryByText(secondTabRe)).toBeInTheDocument(); @@ -50,7 +50,7 @@ describe('LighthouseIntro', () => { describe('closing', () => { it('hides the content on click', () => { - const rendered = render(wrapInThemedTestApp()); + const rendered = render(wrapInTestApp()); const welcomeMessage = rendered.queryByText( 'Welcome to Lighthouse in Backstage!', ); From 44f61015f5eb0a0cbfdfaf44e1b0a02f74b16b2b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 1 Jun 2020 14:01:05 +0200 Subject: [PATCH 62/97] packages/test-utils: add missing wrappapper props --- packages/test-utils/src/testUtils/appWrappers.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index c5839610d3..7849ed20cc 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -14,19 +14,24 @@ * limitations under the License. */ -import React, { ComponentType, ReactNode, FunctionComponent } from 'react'; +import React, { ComponentType, ReactNode, FunctionComponent, FC } from 'react'; import { MemoryRouter } from 'react-router'; import { Route } from 'react-router-dom'; import { lightTheme } from '@backstage/theme'; import privateExports, { defaultSystemIcons, ApiTestRegistry, + BootErrorPageProps, } from '@backstage/core-api'; const { PrivateAppImpl } = privateExports; const NotFoundErrorPage = () => { throw new Error('Reached NotFound Page'); }; +const BootErrorPage: FC = ({ step, error }) => { + throw new Error(`Reached BootError Page at step ${step} with error ${error}`); +}; +const Progress = () =>
; /** * Options to customize the behavior of the test app wrapper. @@ -48,6 +53,8 @@ export function wrapInTestApp( apis: new ApiTestRegistry(), components: { NotFoundErrorPage, + BootErrorPage, + Progress, }, icons: defaultSystemIcons, plugins: [], @@ -59,6 +66,7 @@ export function wrapInTestApp( variant: 'light', }, ], + configLoader: async () => ({}), }); let Wrapper: ComponentType; From 9c415c351fd4262afb3a82898309b3cb484f6d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Mon, 1 Jun 2020 14:24:27 +0200 Subject: [PATCH 63/97] Add ADR link to README (#1069) * Add ADR link to README * Update README.md * rm extra line --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 2e22818f4b..c1ca77c2cc 100644 --- a/README.md +++ b/README.md @@ -82,12 +82,11 @@ Take a look at the [Getting Started](docs/getting-started/README.md) guide to le - [Getting Started](docs/getting-started/README.md) - [Create a Backstage App](docs/create-an-app.md) -- [Architecture](docs/architecture-terminology.md) +- [Architecture](docs/architecture-terminology.md) ([Decisions](docs/architecture-decisions)) - [API references](docs/reference/README.md) - [Designing for Backstage](docs/design.md) - [Storybook - UI components](http://storybook.backstage.io) - [Contributing to Storybook](docs/getting-started/contributing-to-storybook.md) -- Using Backstage components (TODO) ## Contributing From c2877e0a1acea51d27db2f9cb0121f0030077394 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Sun, 31 May 2020 14:50:59 +0200 Subject: [PATCH 64/97] Add PassportStrategyHelper tests. Move util fns to OAuthProvider class. --- .../src/providers/OAuthProvider.test.ts | 21 ++ .../src/providers/OAuthProvider.ts | 39 +++- .../providers/PassportStrategyHelper.test.ts | 214 ++++++++++++++++++ plugins/auth-backend/src/providers/utils.ts | 50 ---- 4 files changed, 272 insertions(+), 52 deletions(-) create mode 100644 plugins/auth-backend/src/providers/OAuthProvider.test.ts create mode 100644 plugins/auth-backend/src/providers/PassportStrategyHelper.test.ts delete mode 100644 plugins/auth-backend/src/providers/utils.ts diff --git a/plugins/auth-backend/src/providers/OAuthProvider.test.ts b/plugins/auth-backend/src/providers/OAuthProvider.test.ts new file mode 100644 index 0000000000..308849057b --- /dev/null +++ b/plugins/auth-backend/src/providers/OAuthProvider.test.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +describe('OAuthProvider', () => { + it('unbreak test runner', () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/plugins/auth-backend/src/providers/OAuthProvider.ts b/plugins/auth-backend/src/providers/OAuthProvider.ts index 81f5ed25a6..822cd5b267 100644 --- a/plugins/auth-backend/src/providers/OAuthProvider.ts +++ b/plugins/auth-backend/src/providers/OAuthProvider.ts @@ -16,9 +16,12 @@ import express, { CookieOptions } from 'express'; import crypto from 'crypto'; -import { AuthProviderRouteHandlers, OAuthProviderHandlers } from './types'; +import { + AuthResponse, + AuthProviderRouteHandlers, + OAuthProviderHandlers, +} from './types'; import { InputError } from '@backstage/backend-common'; -import { postMessageResponse, ensuresXRequestedWith } from './utils'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -86,6 +89,38 @@ export const removeRefreshTokenCookie = ( res.cookie(`${provider}-refresh-token`, '', options); }; +export const postMessageResponse = ( + res: express.Response, + data: AuthResponse, +) => { + const jsonData = JSON.stringify(data); + const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); + + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Frame-Options', 'sameorigin'); + + // TODO: Make target app origin configurable globally + res.end(` + + + + + + `); +}; + +export const ensuresXRequestedWith = (req: express.Request) => { + const requiredHeader = req.header('X-Requested-With'); + + if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { + return false; + } + return true; +}; + export class OAuthProvider implements AuthProviderRouteHandlers { private readonly provider: string; private readonly providerHandlers: OAuthProviderHandlers; diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/providers/PassportStrategyHelper.test.ts new file mode 100644 index 0000000000..e977ee877d --- /dev/null +++ b/plugins/auth-backend/src/providers/PassportStrategyHelper.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import passport from 'passport'; +import { + executeRedirectStrategy, + executeFrameHandlerStrategy, + executeRefreshTokenStrategy, +} from './PassportStrategyHelper'; + +const mockRequest = ({} as unknown) as express.Request; + +describe('PassportStrategyHelper', () => { + class MyCustomRedirectStrategy extends passport.Strategy { + authenticate() { + this.redirect('a', 302); + } + } + + describe('executeRedirectStrategy', () => { + it('should call authenticate and resolve with RedirectInfo', async () => { + const mockStrategy = new MyCustomRedirectStrategy(); + const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate'); + const redirectStrategyPromise = executeRedirectStrategy( + mockRequest, + mockStrategy, + {}, + ); + expect(spyAuthenticate).toBeCalledTimes(1); + await expect(redirectStrategyPromise).resolves.toStrictEqual( + expect.objectContaining({ url: 'a', status: 302 }), + ); + }); + }); + + describe('executeFrameHandlerStrategy', () => { + class MyCustomAuthSuccessStrategy extends passport.Strategy { + authenticate() { + this.success( + { accessToken: 'ACCESS_TOKEN' }, + { refreshToken: 'REFRESH_TOKEN' }, + ); + } + } + class MyCustomAuthErrorStrategy extends passport.Strategy { + authenticate() { + this.error(new Error('MyCustomAuth error')); + } + } + class MyCustomAuthRedirectStrategy extends passport.Strategy { + authenticate() { + this.redirect('URL', 302); + } + } + class MyCustomAuthFailStrategy extends passport.Strategy { + authenticate() { + this.fail('challenge', 302); + } + } + + it('should resolve with user and info on success', async () => { + const mockStrategy = new MyCustomAuthSuccessStrategy(); + const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate'); + const frameHandlerStrategyPromise = executeFrameHandlerStrategy( + mockRequest, + mockStrategy, + ); + expect(spyAuthenticate).toBeCalledTimes(1); + await expect(frameHandlerStrategyPromise).resolves.toStrictEqual( + expect.objectContaining({ + user: { accessToken: 'ACCESS_TOKEN' }, + info: { refreshToken: 'REFRESH_TOKEN' }, + }), + ); + }); + + it('should reject on error', async () => { + const mockStrategy = new MyCustomAuthErrorStrategy(); + const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate'); + const frameHandlerStrategyPromise = executeFrameHandlerStrategy( + mockRequest, + mockStrategy, + ); + expect(spyAuthenticate).toBeCalledTimes(1); + await expect(frameHandlerStrategyPromise).rejects.toThrow( + 'Authentication failed, Error: MyCustomAuth error', + ); + }); + + it('should reject on redirect', async () => { + const mockStrategy = new MyCustomAuthRedirectStrategy(); + const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate'); + const frameHandlerStrategyPromise = executeFrameHandlerStrategy( + mockRequest, + mockStrategy, + ); + expect(spyAuthenticate).toBeCalledTimes(1); + await expect(frameHandlerStrategyPromise).rejects.toThrow( + 'Unexpected redirect', + ); + }); + + it('should reject on fail', async () => { + const mockStrategy = new MyCustomAuthFailStrategy(); + const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate'); + const frameHandlerStrategyPromise = executeFrameHandlerStrategy( + mockRequest, + mockStrategy, + ); + expect(spyAuthenticate).toBeCalledTimes(1); + await expect(frameHandlerStrategyPromise).rejects.toThrow(); + }); + }); + + describe('executeRefreshTokenStrategy', () => { + it('should resolve with a new access token, scope and expiry', async () => { + class MyCustomOAuth2Success { + getOAuthAccessToken( + _refreshToken: string, + _options: any, + callback: Function, + ) { + callback(null, 'ACCESS_TOKEN', 'REFRESH_TOKEN', { + scope: 'a', + expires_in: 10, + }); + } + } + class MyCustomRefreshTokenSuccess extends passport.Strategy { + // @ts-ignore + private _oauth2 = new MyCustomOAuth2Success(); + } + + const mockStrategy = new MyCustomRefreshTokenSuccess(); + const refreshTokenPromise = executeRefreshTokenStrategy( + mockStrategy, + 'REFRESH_TOKEN', + 'a', + ); + await expect(refreshTokenPromise).resolves.toStrictEqual( + expect.objectContaining({ + accessToken: 'ACCESS_TOKEN', + params: expect.objectContaining({ scope: 'a', expires_in: 10 }), + }), + ); + }); + + it('should reject with an error if refresh failed', async () => { + class MyCustomOAuth2Error { + getOAuthAccessToken( + _refreshToken: string, + _options: any, + callback: Function, + ) { + callback(new Error('Unknown error')); + } + } + class MyCustomRefreshTokenSuccess extends passport.Strategy { + // @ts-ignore + private _oauth2 = new MyCustomOAuth2Error(); + } + + const mockStrategy = new MyCustomRefreshTokenSuccess(); + const refreshTokenPromise = executeRefreshTokenStrategy( + mockStrategy, + 'REFRESH_TOKEN', + 'a', + ); + await expect(refreshTokenPromise).rejects.toThrow( + 'Failed to refresh access token Error: Unknown error', + ); + }); + + it('should reject with an error if access token missing in refresh callback', async () => { + class MyCustomOAuth2AccessTokenMissing { + getOAuthAccessToken( + _refreshToken: string, + _options: any, + callback: Function, + ) { + callback(null, ''); + } + } + class MyCustomRefreshTokenSuccess extends passport.Strategy { + // @ts-ignore + private _oauth2 = new MyCustomOAuth2AccessTokenMissing(); + } + + const mockStrategy = new MyCustomRefreshTokenSuccess(); + const refreshTokenPromise = executeRefreshTokenStrategy( + mockStrategy, + 'REFRESH_TOKEN', + 'a', + ); + await expect(refreshTokenPromise).rejects.toThrow( + 'Failed to refresh access token, no access token received', + ); + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/utils.ts b/plugins/auth-backend/src/providers/utils.ts deleted file mode 100644 index 83229e55d4..0000000000 --- a/plugins/auth-backend/src/providers/utils.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import express from 'express'; -import { AuthResponse } from './types'; - -export const postMessageResponse = ( - res: express.Response, - data: AuthResponse, -) => { - const jsonData = JSON.stringify(data); - const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); - - res.setHeader('Content-Type', 'text/html'); - res.setHeader('X-Frame-Options', 'sameorigin'); - - // TODO: Make target app origin configurable globally - res.end(` - - - - - - `); -}; - -export const ensuresXRequestedWith = (req: express.Request) => { - const requiredHeader = req.header('X-Requested-With'); - - if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { - return false; - } - return true; -}; From a90e6ab44ff3470d5bcbd7fe1ada2bfe7f21fafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 1 Jun 2020 14:47:09 +0200 Subject: [PATCH 65/97] Update plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts Co-authored-by: Patrik Oldsberg --- packages/backend-common/src/index.ts | 1 - .../src/testing/MockedMemberFunctions.ts | 35 ------------------- packages/backend-common/src/testing/index.ts | 17 --------- .../catalog/DatabaseEntitiesCatalog.test.ts | 3 +- 4 files changed, 1 insertion(+), 55 deletions(-) delete mode 100644 packages/backend-common/src/testing/MockedMemberFunctions.ts delete mode 100644 packages/backend-common/src/testing/index.ts diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index 4bc60f557f..b2c38ab506 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -17,4 +17,3 @@ export * from './errors'; export * from './logging'; export * from './middleware'; -export * from './testing'; diff --git a/packages/backend-common/src/testing/MockedMemberFunctions.ts b/packages/backend-common/src/testing/MockedMemberFunctions.ts deleted file mode 100644 index 9b35908bff..0000000000 --- a/packages/backend-common/src/testing/MockedMemberFunctions.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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. - */ - -/** - * For any type T, generate a new type that is identical but also has the - * jest.fn signature on all member functions. - * - * When writing tests against a type, you sometimes end up in a situation where - * you need to write expect(x.y as jest.Mock).toHaveBeenCalled... because the - * x.y member was considered to be the actual function type. You could also - * change your test to instead create a "raw" object { y: jest.fn() } but then - * you lose type safety when doing x.y.mockReturnValue(...). So you start - * trying to do { y: jest.fn() as X['y'] } as X or similar trickery. - * - * This type lets you say const x: MockedMemberFunctions = { y: jest.fn() } - * and keep all the type safety at every step. - */ -export type MockedMemberFunctions = { - [K in keyof T]: T[K] extends (...args: infer A) => infer B - ? T[K] & jest.Mock - : T[K]; -}; diff --git a/packages/backend-common/src/testing/index.ts b/packages/backend-common/src/testing/index.ts deleted file mode 100644 index c12297465b..0000000000 --- a/packages/backend-common/src/testing/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export type { MockedMemberFunctions } from './MockedMemberFunctions'; diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts index c5101dff7a..1de5038112 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -14,13 +14,12 @@ * limitations under the License. */ -import type { MockedMemberFunctions } from '@backstage/backend-common'; import type { Entity, EntityPolicy } from '@backstage/catalog-model'; import type { Database } from '../database'; import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; describe('DatabaseEntitiesCatalog', () => { - let db: MockedMemberFunctions; + let db: jest.Mocked; let policy: EntityPolicy; beforeEach(() => { From 65ab1685f4d71dda10892902f545d79c27eb2a99 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 1 Jun 2020 14:58:52 +0200 Subject: [PATCH 66/97] packages/core-api: make App configLoader optional and synchronous when missing --- packages/core-api/src/app/App.tsx | 10 ++++++---- packages/test-utils/src/testUtils/appWrappers.tsx | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 49d2291acd..4207a267af 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -43,7 +43,7 @@ type FullAppOptions = { plugins: BackstagePlugin[]; components: AppComponents; themes: AppTheme[]; - configLoader: AppConfigLoader; + configLoader?: AppConfigLoader; }; export class PrivateAppImpl implements BackstageApp { @@ -52,7 +52,7 @@ export class PrivateAppImpl implements BackstageApp { private readonly plugins: BackstagePlugin[]; private readonly components: AppComponents; private readonly themes: AppTheme[]; - private readonly configLoader: AppConfigLoader; + private readonly configLoader?: AppConfigLoader; constructor(options: FullAppOptions) { this.apis = options.apis; @@ -148,11 +148,13 @@ export class PrivateAppImpl implements BackstageApp { getProvider(): ComponentType<{}> { const Provider: FC<{}> = ({ children }) => { - const config = useAsync(this.configLoader); + // Keeping this synchronous when a config loader isn't set simplifies tests a lot + const hasConfig = Boolean(this.configLoader); + const config = useAsync(this.configLoader || (() => Promise.resolve({}))); let childNode = children; - if (config.loading) { + if (hasConfig && config.loading) { const { Progress } = this.components; childNode = ; } else if (config.error) { diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 7849ed20cc..2450604976 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -66,7 +66,6 @@ export function wrapInTestApp( variant: 'light', }, ], - configLoader: async () => ({}), }); let Wrapper: ComponentType; From 9a6df0bf18dd17f8994ca0bf84d25f7081a910e7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 May 2020 10:20:22 +0200 Subject: [PATCH 67/97] packages/cli: call eslint directly --- packages/cli/config/eslint.backend.js | 2 +- packages/cli/config/eslint.js | 2 +- packages/cli/src/commands/lint.ts | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index 619d3c72e0..4f45e232b2 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -32,7 +32,7 @@ module.exports = { ecmaVersion: 2018, sourceType: 'module', }, - ignorePatterns: ['**/dist/**', '**/build/**'], + ignorePatterns: ['.eslintrc.js', '**/dist/**'], rules: { 'no-console': 0, // Permitted in console programs 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 0c6bfbe8e7..922867ea5a 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -39,7 +39,7 @@ module.exports = { version: 'detect', }, }, - ignorePatterns: ['**/dist/**', '**/build/**'], + ignorePatterns: ['.eslintrc.js', '**/dist/**'], rules: { 'import/no-duplicates': 'warn', 'import/no-extraneous-dependencies': [ diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 07819bacfb..7eeedcbe29 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -16,12 +16,19 @@ import { Command } from 'commander'; import { run } from '../lib/run'; +import { paths } from '../lib/paths'; export default async (cmd: Command) => { - const args = ['lint', '--max-warnings=0', '--format=codeframe']; + const args = [ + '--ext', + 'js,jsx,ts,tsx', + '--max-warnings=0', + '--format=codeframe', + paths.targetDir, + ]; if (cmd.fix) { args.push('--fix'); } - await run('web-scripts', args); + await run('eslint', args); }; From f67f928afc4a561784db45aa0e2f4c5b0fb0b9c1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 May 2020 10:52:00 +0200 Subject: [PATCH 68/97] packages/cli: remove direct web-scripts dependency and use config packages instead --- packages/cli/config/tsconfig.json | 2 +- packages/cli/package.json | 5 +- yarn.lock | 3007 +++-------------------------- 3 files changed, 260 insertions(+), 2754 deletions(-) diff --git a/packages/cli/config/tsconfig.json b/packages/cli/config/tsconfig.json index 652e990387..fb52420860 100644 --- a/packages/cli/config/tsconfig.json +++ b/packages/cli/config/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@spotify/web-scripts/config/tsconfig.json", + "extends": "@spotify/tsconfig", "exclude": ["**/*.test.*"], "compilerOptions": { "allowJs": true, diff --git a/packages/cli/package.json b/packages/cli/package.json index a15765c731..13c0fd4aa3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -35,7 +35,8 @@ "@rollup/plugin-commonjs": "^11.0.2", "@rollup/plugin-json": "^4.0.2", "@rollup/plugin-node-resolve": "^7.1.1", - "@spotify/web-scripts": "^6.0.0", + "@spotify/eslint-config": "^7.0.1", + "@spotify/tsconfig": "^7.0.0", "@sucrase/webpack-loader": "^2.0.0", "bfj": "^7.0.2", "chalk": "^4.0.0", @@ -44,6 +45,7 @@ "css-loader": "^3.5.3", "dashify": "^2.0.0", "diff": "^4.0.2", + "eslint": "^7.1.0", "eslint-plugin-import": "^2.20.2", "eslint-plugin-monorepo": "^0.2.1", "fork-ts-checker-webpack-plugin": "^4.0.5", @@ -74,6 +76,7 @@ "tar": "^6.0.1", "ts-jest": "^26.0.0", "ts-loader": "^7.0.4", + "typescript": "^3.9.3", "url-loader": "^4.1.0", "webpack": "^4.41.6", "webpack-dev-server": "^3.10.3", diff --git a/yarn.lock b/yarn.lock index 80e3916f53..679f75cb36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -378,7 +378,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-bigint@^7.0.0", "@babel/plugin-syntax-bigint@^7.8.3": +"@babel/plugin-syntax-bigint@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== @@ -441,7 +441,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": +"@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== @@ -937,136 +937,6 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@commitlint/cli@^8.3.3": - version "8.3.5" - resolved "https://registry.npmjs.org/@commitlint/cli/-/cli-8.3.5.tgz#6d93a3a8b2437fa978999d3f6a336bcc70be3fd3" - integrity sha512-6+L0vbw55UEdht71pgWOE55SRgb+8OHcEwGDB234VlIBFGK9P2QOBU7MHiYJ5cjdjCQ0rReNrGjOHmJ99jwf0w== - dependencies: - "@commitlint/format" "^8.3.4" - "@commitlint/lint" "^8.3.5" - "@commitlint/load" "^8.3.5" - "@commitlint/read" "^8.3.4" - babel-polyfill "6.26.0" - chalk "2.4.2" - get-stdin "7.0.0" - lodash "4.17.15" - meow "5.0.0" - resolve-from "5.0.0" - resolve-global "1.0.0" - -"@commitlint/config-conventional@^8.3.3": - version "8.3.4" - resolved "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-8.3.4.tgz#fed13b3711690663b176c1f6b39c205a565618d2" - integrity sha512-w0Yc5+aVAjZgjYqx29igBOnVCj8O22gy3Vo6Fyp7PwoS7+AYS1x3sN7IBq6i7Ae15Mv5P+rEx1pkxXo5zOMe4g== - dependencies: - conventional-changelog-conventionalcommits "4.2.1" - -"@commitlint/ensure@^8.3.4": - version "8.3.4" - resolved "https://registry.npmjs.org/@commitlint/ensure/-/ensure-8.3.4.tgz#6931677e4ca0fde71686ae3b7a367261647a341d" - integrity sha512-8NW77VxviLhD16O3EUd02lApMFnrHexq10YS4F4NftNoErKbKaJ0YYedktk2boKrtNRf/gQHY/Qf65edPx4ipw== - dependencies: - lodash "4.17.15" - -"@commitlint/execute-rule@^8.3.4": - version "8.3.4" - resolved "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-8.3.4.tgz#1b63f0713b197889d90b76f9eea1abc010d256b1" - integrity sha512-f4HigYjeIBn9f7OuNv5zh2y5vWaAhNFrfeul8CRJDy82l3Y+09lxOTGxfF3uMXKrZq4LmuK6qvvRCZ8mUrVvzQ== - -"@commitlint/format@^8.3.4": - version "8.3.4" - resolved "https://registry.npmjs.org/@commitlint/format/-/format-8.3.4.tgz#7cd1f0ba5a3289c8d14d7dac29ee1fc1597fe1d9" - integrity sha512-809wlQ/ND6CLZON+w2Rb3YM2TLNDfU2xyyqpZeqzf2reJNpySMSUAeaO/fNDJSOKIsOsR3bI01rGu6hv28k+Nw== - dependencies: - chalk "^2.0.1" - -"@commitlint/is-ignored@^8.3.5": - version "8.3.5" - resolved "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-8.3.5.tgz#e6f59496e1b1ce58020d519cd578ad0f43169199" - integrity sha512-Zo+8a6gJLFDTqyNRx53wQi/XTiz8mncvmWf/4oRG+6WRcBfjSSHY7KPVj5Y6UaLy2EgZ0WQ2Tt6RdTDeQiQplA== - dependencies: - semver "6.3.0" - -"@commitlint/lint@^8.3.5": - version "8.3.5" - resolved "https://registry.npmjs.org/@commitlint/lint/-/lint-8.3.5.tgz#627e75adb1cc803cc723e33cc2ba4aa27cbb9f0c" - integrity sha512-02AkI0a6PU6rzqUvuDkSi6rDQ2hUgkq9GpmdJqfai5bDbxx2939mK4ZO+7apbIh4H6Pae7EpYi7ffxuJgm+3hQ== - dependencies: - "@commitlint/is-ignored" "^8.3.5" - "@commitlint/parse" "^8.3.4" - "@commitlint/rules" "^8.3.4" - babel-runtime "^6.23.0" - lodash "4.17.15" - -"@commitlint/load@>6.1.1", "@commitlint/load@^8.3.5": - version "8.3.5" - resolved "https://registry.npmjs.org/@commitlint/load/-/load-8.3.5.tgz#3f059225ede92166ba94cf4c48e3d67c8b08b18a" - integrity sha512-poF7R1CtQvIXRmVIe63FjSQmN9KDqjRtU5A6hxqXBga87yB2VUJzic85TV6PcQc+wStk52cjrMI+g0zFx+Zxrw== - dependencies: - "@commitlint/execute-rule" "^8.3.4" - "@commitlint/resolve-extends" "^8.3.5" - babel-runtime "^6.23.0" - chalk "2.4.2" - cosmiconfig "^5.2.0" - lodash "4.17.15" - resolve-from "^5.0.0" - -"@commitlint/message@^8.3.4": - version "8.3.4" - resolved "https://registry.npmjs.org/@commitlint/message/-/message-8.3.4.tgz#b4e50d14aa6e15a5ad0767b952a7953f3681d768" - integrity sha512-nEj5tknoOKXqBsaQtCtgPcsAaf5VCg3+fWhss4Vmtq40633xLq0irkdDdMEsYIx8rGR0XPBTukqzln9kAWCkcA== - -"@commitlint/parse@^8.3.4": - version "8.3.4" - resolved "https://registry.npmjs.org/@commitlint/parse/-/parse-8.3.4.tgz#d741f8b9104b35d0f4c10938165b20cbf167f81e" - integrity sha512-b3uQvpUQWC20EBfKSfMRnyx5Wc4Cn778bVeVOFErF/cXQK725L1bYFvPnEjQO/GT8yGVzq2wtLaoEqjm1NJ/Bw== - dependencies: - conventional-changelog-angular "^1.3.3" - conventional-commits-parser "^3.0.0" - lodash "^4.17.11" - -"@commitlint/read@^8.3.4": - version "8.3.4" - resolved "https://registry.npmjs.org/@commitlint/read/-/read-8.3.4.tgz#81a34283d8cd7b2acdf57829a91761e9c7791455" - integrity sha512-FKv1kHPrvcAG5j+OSbd41IWexsbLhfIXpxVC/YwQZO+FR0EHmygxQNYs66r+GnhD1EfYJYM4WQIqd5bJRx6OIw== - dependencies: - "@commitlint/top-level" "^8.3.4" - "@marionebl/sander" "^0.6.0" - babel-runtime "^6.23.0" - git-raw-commits "^2.0.0" - -"@commitlint/resolve-extends@^8.3.5": - version "8.3.5" - resolved "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-8.3.5.tgz#8fff800f292ac217ae30b1862f5f9a84b278310a" - integrity sha512-nHhFAK29qiXNe6oH6uG5wqBnCR+BQnxlBW/q5fjtxIaQALgfoNLHwLS9exzbIRFqwJckpR6yMCfgMbmbAOtklQ== - dependencies: - import-fresh "^3.0.0" - lodash "4.17.15" - resolve-from "^5.0.0" - resolve-global "^1.0.0" - -"@commitlint/rules@^8.3.4": - version "8.3.4" - resolved "https://registry.npmjs.org/@commitlint/rules/-/rules-8.3.4.tgz#41da7e16c6b89af268fe81c87a158c1fd2ac82b1" - integrity sha512-xuC9dlqD5xgAoDFgnbs578cJySvwOSkMLQyZADb1xD5n7BNcUJfP8WjT9W1Aw8K3Wf8+Ym/ysr9FZHXInLeaRg== - dependencies: - "@commitlint/ensure" "^8.3.4" - "@commitlint/message" "^8.3.4" - "@commitlint/to-lines" "^8.3.4" - babel-runtime "^6.23.0" - -"@commitlint/to-lines@^8.3.4": - version "8.3.4" - resolved "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-8.3.4.tgz#ce24963b6d86dbe51d88d5e3028ab28f38562e2e" - integrity sha512-5AvcdwRsMIVq0lrzXTwpbbG5fKRTWcHkhn/hCXJJ9pm1JidsnidS1y0RGkb3O50TEHGewhXwNoavxW9VToscUA== - -"@commitlint/top-level@^8.3.4": - version "8.3.4" - resolved "https://registry.npmjs.org/@commitlint/top-level/-/top-level-8.3.4.tgz#803fc6e8f5be5efa5f3551761acfca961f1d8685" - integrity sha512-nOaeLBbAqSZNpKgEtO6NAxmui1G8ZvLG+0wb4rvv6mWhPDzK1GNZkCd8FUZPahCoJ1iHDoatw7F8BbJLg4nDjg== - dependencies: - find-up "^4.0.0" - "@cypress/listr-verbose-renderer@0.4.1": version "0.4.1" resolved "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" @@ -1412,15 +1282,6 @@ prop-types "^15.6.2" scheduler "^0.19.0" -"@iarna/cli@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@iarna/cli/-/cli-1.2.0.tgz#0f7af5e851afe895104583c4ca07377a8094d641" - integrity sha512-ukITQAqVs2n9HGmn3car/Ir7d3ta650iXhrG7pjr3EWdFmJuuOVWgYsu7ftsSe5VifEFFhjxVuX9+8F7L8hwcA== - dependencies: - signal-exit "^3.0.2" - update-notifier "^2.2.0" - yargs "^8.0.2" - "@istanbuljs/load-nyc-config@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" @@ -1436,16 +1297,6 @@ resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jest/console@^25.1.0": - version "25.1.0" - resolved "https://registry.npmjs.org/@jest/console/-/console-25.1.0.tgz#1fc765d44a1e11aec5029c08e798246bd37075ab" - integrity sha512-3P1DpqAMK/L07ag/Y9/Jup5iDEG9P4pRAuZiMQnU0JB3UOvCyYCjCoxr7sIA80SeyUCUKrr24fKAxVpmBgQonA== - dependencies: - "@jest/source-map" "^25.1.0" - chalk "^3.0.0" - jest-util "^25.1.0" - slash "^3.0.0" - "@jest/console@^26.0.1": version "26.0.1" resolved "https://registry.npmjs.org/@jest/console/-/console-26.0.1.tgz#62b3b2fa8990f3cbffbef695c42ae9ddbc8f4b39" @@ -1457,40 +1308,6 @@ jest-util "^26.0.1" slash "^3.0.0" -"@jest/core@^25.1.0": - version "25.1.0" - resolved "https://registry.npmjs.org/@jest/core/-/core-25.1.0.tgz#3d4634fc3348bb2d7532915d67781cdac0869e47" - integrity sha512-iz05+NmwCmZRzMXvMo6KFipW7nzhbpEawrKrkkdJzgytavPse0biEnCNr2wRlyCsp3SmKaEY+SGv7YWYQnIdig== - dependencies: - "@jest/console" "^25.1.0" - "@jest/reporters" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" - ansi-escapes "^4.2.1" - chalk "^3.0.0" - exit "^0.1.2" - graceful-fs "^4.2.3" - jest-changed-files "^25.1.0" - jest-config "^25.1.0" - jest-haste-map "^25.1.0" - jest-message-util "^25.1.0" - jest-regex-util "^25.1.0" - jest-resolve "^25.1.0" - jest-resolve-dependencies "^25.1.0" - jest-runner "^25.1.0" - jest-runtime "^25.1.0" - jest-snapshot "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" - jest-watcher "^25.1.0" - micromatch "^4.0.2" - p-each-series "^2.1.0" - realpath-native "^1.1.0" - rimraf "^3.0.0" - slash "^3.0.0" - strip-ansi "^6.0.0" - "@jest/core@^26.0.1": version "26.0.1" resolved "https://registry.npmjs.org/@jest/core/-/core-26.0.1.tgz#aa538d52497dfab56735efb00e506be83d841fae" @@ -1524,15 +1341,6 @@ slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^25.1.0": - version "25.1.0" - resolved "https://registry.npmjs.org/@jest/environment/-/environment-25.1.0.tgz#4a97f64770c9d075f5d2b662b5169207f0a3f787" - integrity sha512-cTpUtsjU4cum53VqBDlcW0E4KbQF03Cn0jckGPW/5rrE9tb+porD3+hhLtHAwhthsqfyF+bizyodTlsRA++sHg== - dependencies: - "@jest/fake-timers" "^25.1.0" - "@jest/types" "^25.1.0" - jest-mock "^25.1.0" - "@jest/environment@^26.0.1": version "26.0.1" resolved "https://registry.npmjs.org/@jest/environment/-/environment-26.0.1.tgz#82f519bba71959be9b483675ee89de8c8f72a5c8" @@ -1542,17 +1350,6 @@ "@jest/types" "^26.0.1" jest-mock "^26.0.1" -"@jest/fake-timers@^25.1.0": - version "25.1.0" - resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.1.0.tgz#a1e0eff51ffdbb13ee81f35b52e0c1c11a350ce8" - integrity sha512-Eu3dysBzSAO1lD7cylZd/CVKdZZ1/43SF35iYBNV1Lvvn2Undp3Grwsv8PrzvbLhqwRzDd4zxrY4gsiHc+wygQ== - dependencies: - "@jest/types" "^25.1.0" - jest-message-util "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" - lolex "^5.0.0" - "@jest/fake-timers@^26.0.1": version "26.0.1" resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.0.1.tgz#f7aeff13b9f387e9d0cac9a8de3bba538d19d796" @@ -1573,39 +1370,6 @@ "@jest/types" "^26.0.1" expect "^26.0.1" -"@jest/reporters@^25.1.0": - version "25.1.0" - resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-25.1.0.tgz#9178ecf136c48f125674ac328f82ddea46e482b0" - integrity sha512-ORLT7hq2acJQa8N+NKfs68ZtHFnJPxsGqmofxW7v7urVhzJvpKZG9M7FAcgh9Ee1ZbCteMrirHA3m5JfBtAaDg== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^25.1.0" - "@jest/environment" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" - chalk "^3.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.2" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.0" - jest-haste-map "^25.1.0" - jest-resolve "^25.1.0" - jest-runtime "^25.1.0" - jest-util "^25.1.0" - jest-worker "^25.1.0" - slash "^3.0.0" - source-map "^0.6.0" - string-length "^3.1.0" - terminal-link "^2.0.0" - v8-to-istanbul "^4.0.1" - optionalDependencies: - node-notifier "^6.0.0" - "@jest/reporters@^26.0.1": version "26.0.1" resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-26.0.1.tgz#14ae00e7a93e498cec35b0c00ab21c375d9b078f" @@ -1638,15 +1402,6 @@ optionalDependencies: node-notifier "^7.0.0" -"@jest/source-map@^25.1.0": - version "25.1.0" - resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-25.1.0.tgz#b012e6c469ccdbc379413f5c1b1ffb7ba7034fb0" - integrity sha512-ohf2iKT0xnLWcIUhL6U6QN+CwFWf9XnrM2a6ybL9NXxJjgYijjLSitkYHIdzkd8wFliH73qj/+epIpTiWjRtAA== - dependencies: - callsites "^3.0.0" - graceful-fs "^4.2.3" - source-map "^0.6.0" - "@jest/source-map@^26.0.0": version "26.0.0" resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-26.0.0.tgz#fd7706484a7d3faf7792ae29783933bbf48a4749" @@ -1656,17 +1411,6 @@ graceful-fs "^4.2.4" source-map "^0.6.0" -"@jest/test-result@^25.1.0": - version "25.1.0" - resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-25.1.0.tgz#847af2972c1df9822a8200457e64be4ff62821f7" - integrity sha512-FZzSo36h++U93vNWZ0KgvlNuZ9pnDnztvaM7P/UcTx87aPDotG18bXifkf1Ji44B7k/eIatmMzkBapnAzjkJkg== - dependencies: - "@jest/console" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" - "@jest/test-result@^26.0.1": version "26.0.1" resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-26.0.1.tgz#1ffdc1ba4bc289919e54b9414b74c9c2f7b2b718" @@ -1677,16 +1421,6 @@ "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^25.1.0": - version "25.1.0" - resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.1.0.tgz#4df47208542f0065f356fcdb80026e3c042851ab" - integrity sha512-WgZLRgVr2b4l/7ED1J1RJQBOharxS11EFhmwDqknpknE0Pm87HLZVS2Asuuw+HQdfQvm2aXL2FvvBLxOD1D0iw== - dependencies: - "@jest/test-result" "^25.1.0" - jest-haste-map "^25.1.0" - jest-runner "^25.1.0" - jest-runtime "^25.1.0" - "@jest/test-sequencer@^26.0.1": version "26.0.1" resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.0.1.tgz#b0563424728f3fe9e75d1442b9ae4c11da73f090" @@ -1698,28 +1432,6 @@ jest-runner "^26.0.1" jest-runtime "^26.0.1" -"@jest/transform@^25.1.0": - version "25.1.0" - resolved "https://registry.npmjs.org/@jest/transform/-/transform-25.1.0.tgz#221f354f512b4628d88ce776d5b9e601028ea9da" - integrity sha512-4ktrQ2TPREVeM+KxB4zskAT84SnmG1vaz4S+51aTefyqn3zocZUnliLLm5Fsl85I3p/kFPN4CRp1RElIfXGegQ== - dependencies: - "@babel/core" "^7.1.0" - "@jest/types" "^25.1.0" - babel-plugin-istanbul "^6.0.0" - chalk "^3.0.0" - convert-source-map "^1.4.0" - fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.3" - jest-haste-map "^25.1.0" - jest-regex-util "^25.1.0" - jest-util "^25.1.0" - micromatch "^4.0.2" - pirates "^4.0.1" - realpath-native "^1.1.0" - slash "^3.0.0" - source-map "^0.6.1" - write-file-atomic "^3.0.0" - "@jest/transform@^26.0.1": version "26.0.1" resolved "https://registry.npmjs.org/@jest/transform/-/transform-26.0.1.tgz#0e3ecbb34a11cd4b2080ed0a9c4856cf0ceb0639" @@ -1750,16 +1462,6 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@jest/types@^25.1.0": - version "25.1.0" - resolved "https://registry.npmjs.org/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395" - integrity sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^1.1.1" - "@types/yargs" "^15.0.0" - chalk "^3.0.0" - "@jest/types@^25.5.0": version "25.5.0" resolved "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d" @@ -2483,15 +2185,6 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" -"@marionebl/sander@^0.6.0": - version "0.6.1" - resolved "https://registry.npmjs.org/@marionebl/sander/-/sander-0.6.1.tgz#1958965874f24bc51be48875feb50d642fc41f7b" - integrity sha1-GViWWHTyS8Ub5Ih1/rUNZC/EH3s= - dependencies: - graceful-fs "^4.1.3" - mkdirp "^0.5.1" - rimraf "^2.5.2" - "@material-ui/core@^4.9.1": version "4.9.7" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.9.7.tgz#0c1caf123278770f34c5d8e9ecd9e1314f87a621" @@ -2638,18 +2331,6 @@ dependencies: "@octokit/types" "^2.0.0" -"@octokit/core@^2.4.0": - version "2.4.2" - resolved "https://registry.npmjs.org/@octokit/core/-/core-2.4.2.tgz#c22e583afc97e74015ea5bfd3ffb3ffc56c186ed" - integrity sha512-fUx/Qt774cgiPhb3HRKfdl6iufVL/ltECkwkCg373I4lIPYvAPY4cbidVZqyVqHI+ThAIlFlTW8FT4QHChv3Sg== - dependencies: - "@octokit/auth-token" "^2.4.0" - "@octokit/graphql" "^4.3.1" - "@octokit/request" "^5.3.1" - "@octokit/types" "^2.0.0" - before-after-hook "^2.1.0" - universal-user-agent "^5.0.0" - "@octokit/endpoint@^5.5.0": version "5.5.3" resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.3.tgz#0397d1baaca687a4c8454ba424a627699d97c978" @@ -2659,15 +2340,6 @@ is-plain-object "^3.0.0" universal-user-agent "^5.0.0" -"@octokit/graphql@^4.3.1": - version "4.3.1" - resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz#9ee840e04ed2906c7d6763807632de84cdecf418" - integrity sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA== - dependencies: - "@octokit/request" "^5.3.0" - "@octokit/types" "^2.0.0" - universal-user-agent "^4.0.0" - "@octokit/plugin-enterprise-rest@^3.6.1": version "3.6.2" resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-3.6.2.tgz#74de25bef21e0182b4fa03a8678cd00a4e67e561" @@ -2680,13 +2352,6 @@ dependencies: "@octokit/types" "^2.0.1" -"@octokit/plugin-paginate-rest@^2.0.0": - version "2.0.2" - resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.0.2.tgz#fee7a81a4cc7d03784aaf9225499dd6e27f6d01e" - integrity sha512-HzODcSUt9mjErly26TlTOGZrhf9bmF/FEDQ2zln1izhgmIV6ulsjsHmgmR4VZ0wzVr/m52Eb6U2XuyS8fkcR1A== - dependencies: - "@octokit/types" "^2.0.1" - "@octokit/plugin-request-log@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e" @@ -2700,14 +2365,6 @@ "@octokit/types" "^2.0.1" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@^3.3.0": - version "3.3.1" - resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-3.3.1.tgz#279920ee391bd7e944ae4aa7fa435ba0c51fbb6a" - integrity sha512-iLAXPLWBZaP6ocy1GFfZUCzyN4cwg3y2JE6yZjQo0zLE3UaewC3TI68/TnS4ilyhXDxh81Jr1qwPN1AqTp8t3w== - dependencies: - "@octokit/types" "^2.0.1" - deprecation "^2.3.1" - "@octokit/request-error@^1.0.1", "@octokit/request-error@^1.0.2": version "1.2.1" resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" @@ -2717,7 +2374,7 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.3.1": +"@octokit/request@^5.2.0": version "5.3.2" resolved "https://registry.npmjs.org/@octokit/request/-/request-5.3.2.tgz#1ca8b90a407772a1ee1ab758e7e0aced213b9883" integrity sha512-7NPJpg19wVQy1cs2xqXjjRq/RmtSomja/VSWnptfYwuBxLdbYh2UjhGi0Wx7B1v5Iw5GKhfFDQL7jM7SSp7K2g== @@ -2753,16 +2410,6 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@octokit/rest@^17.0.0": - version "17.1.1" - resolved "https://registry.npmjs.org/@octokit/rest/-/rest-17.1.1.tgz#357a9f6da687fc2ca9276715c1541562bd2c1581" - integrity sha512-Cn9XpevTJdQj/GbACY1WjArDabPpdAhvlS5zoCdZ/chiKbl4vutL1X1VeJHzDLK0K6BdZXXe4SnEPN31wKPA7A== - dependencies: - "@octokit/core" "^2.4.0" - "@octokit/plugin-paginate-rest" "^2.0.0" - "@octokit/plugin-request-log" "^1.0.0" - "@octokit/plugin-rest-endpoint-methods" "^3.3.0" - "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.5.0" resolved "https://registry.npmjs.org/@octokit/types/-/types-2.5.0.tgz#f1bbd147e662ae2c79717d518aac686e58257773" @@ -2825,81 +2472,6 @@ dependencies: any-observable "^0.3.0" -"@semantic-release/commit-analyzer@^8.0.0": - version "8.0.1" - resolved "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-8.0.1.tgz#5d2a37cd5a3312da0e3ac05b1ca348bf60b90bca" - integrity sha512-5bJma/oB7B4MtwUkZC2Bf7O1MHfi4gWe4mA+MIQ3lsEV0b422Bvl1z5HRpplDnMLHH3EXMoRdEng6Ds5wUqA3A== - dependencies: - conventional-changelog-angular "^5.0.0" - conventional-commits-filter "^2.0.0" - conventional-commits-parser "^3.0.7" - debug "^4.0.0" - import-from "^3.0.0" - lodash "^4.17.4" - micromatch "^4.0.2" - -"@semantic-release/error@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@semantic-release/error/-/error-2.2.0.tgz#ee9d5a09c9969eade1ec864776aeda5c5cddbbf0" - integrity sha512-9Tj/qn+y2j+sjCI3Jd+qseGtHjOAeg7dU2/lVcqIQ9TV3QDaDXDYXcoOHU+7o2Hwh8L8ymL4gfuO7KxDs3q2zg== - -"@semantic-release/github@^7.0.0": - version "7.0.5" - resolved "https://registry.npmjs.org/@semantic-release/github/-/github-7.0.5.tgz#042b515cbae8695aa60bc4ed17722c34512a5b89" - integrity sha512-1nJCMeomspRIXKiFO3VXtkUMbIBEreYLFNBdWoLjvlUNcEK0/pEbupEZJA3XHfJuSzv43u3OLpPhF/JBrMuv+A== - dependencies: - "@octokit/rest" "^17.0.0" - "@semantic-release/error" "^2.2.0" - aggregate-error "^3.0.0" - bottleneck "^2.18.1" - debug "^4.0.0" - dir-glob "^3.0.0" - fs-extra "^9.0.0" - globby "^11.0.0" - http-proxy-agent "^4.0.0" - https-proxy-agent "^5.0.0" - issue-parser "^6.0.0" - lodash "^4.17.4" - mime "^2.4.3" - p-filter "^2.0.0" - p-retry "^4.0.0" - url-join "^4.0.0" - -"@semantic-release/npm@^7.0.0": - version "7.0.5" - resolved "https://registry.npmjs.org/@semantic-release/npm/-/npm-7.0.5.tgz#61c45691abb863f6939cca6aac958d3c22508632" - integrity sha512-D+oEmsx9aHE1q806NFQwSC9KdBO8ri/VO99eEz0wWbX2jyLqVyWr7t0IjKC8aSnkkQswg/4KN/ZjfF6iz1XOpw== - dependencies: - "@semantic-release/error" "^2.2.0" - aggregate-error "^3.0.0" - execa "^4.0.0" - fs-extra "^9.0.0" - lodash "^4.17.15" - nerf-dart "^1.0.0" - normalize-url "^5.0.0" - npm "^6.10.3" - rc "^1.2.8" - read-pkg "^5.0.0" - registry-auth-token "^4.0.0" - semver "^7.1.2" - tempy "^0.5.0" - -"@semantic-release/release-notes-generator@^9.0.0": - version "9.0.1" - resolved "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-9.0.1.tgz#732d285d103064f2a64f08a32031551ebb4f918b" - integrity sha512-bOoTiH6SiiR0x2uywSNR7uZcRDl22IpZhj+Q5Bn0v+98MFtOMhCxFhbrKQjhbYoZw7vps1mvMRmFkp/g6R9cvQ== - dependencies: - conventional-changelog-angular "^5.0.0" - conventional-changelog-writer "^4.0.0" - conventional-commits-filter "^2.0.0" - conventional-commits-parser "^3.0.0" - debug "^4.0.0" - get-stream "^5.0.0" - import-from "^3.0.0" - into-stream "^5.0.0" - lodash "^4.17.4" - read-pkg-up "^7.0.0" - "@sheerun/mutationobserver-shim@^0.3.2": version "0.3.3" resolved "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#5405ee8e444ed212db44e79351f0c70a582aae25" @@ -2924,10 +2496,10 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@spotify/eslint-config-base@^6.1.0": - version "6.1.0" - resolved "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-6.1.0.tgz#b48d2764049d56a7d83a95709ec1e6733cb19f2a" - integrity sha512-oxR3OBBrms09Ih6yPrqcaeKgwchdw9XgRvKsF6x6uPiMy0OVYald/+GkBtyQy+IChD14WStV/XosqNxEZeTJ4Q== +"@spotify/eslint-config-base@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-7.0.0.tgz#36804ae09ec938f1aa5f9464ea993f3f151cfaa8" + integrity sha512-XRTrTRyRRYBxPYINKNHw4B8QWlA3p4I+id/Po2sMdejtXH3LZgDgIjdschUZSdQ6J6dVYDdaVykdizRM9I+G6Q== "@spotify/eslint-config-oss@^1.0.1": version "1.0.2" @@ -2936,86 +2508,51 @@ dependencies: eslint-plugin-notice "^0.9.10" -"@spotify/eslint-config-react@^6.0.0": - version "6.0.0" - resolved "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-6.0.0.tgz#9cfc88362c7ffcd14e4dde73c196d1bc8583051d" - integrity sha512-Tb88Yvu9lAjT/isKbebmOoxBAmjreDFH4lQfXxoFcW56Wl78l2yvLo0LkPV8lOx5+lKfX0BbFiBBpwl+jCaflg== +"@spotify/eslint-config-react@^7.0.1": + version "7.0.1" + resolved "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-7.0.1.tgz#2e70de9d7911ea9aeaa55a83a332220a28f6c431" + integrity sha512-HNDHvm19EaBXiDgsg52mjMgKWZTeA0hO2Q75ACNwb8UtjIKkANqyyuzyDGo8jiGMbWIm6wJjShlRtT95emNlqQ== -"@spotify/eslint-config-typescript@^6.0.0": - version "6.0.0" - resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-6.0.0.tgz#8ae8469d2a0e219d71abaf2da14f8659c3912a34" - integrity sha512-cK1iHhfMgvZFPANqLoChhgUQ1oqH+qTWILGvcsc6HylzVt9/kM0SyjGcDuZKj+CP6TecJODdyHgb5U6U+O5gKw== +"@spotify/eslint-config-typescript@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-7.0.0.tgz#fc5227e3344f74b41ac3a530df24a95ac13254e4" + integrity sha512-28I/SAf68NKbWZ5IY0WYMa0D18PxWdC9DP9gRbOTlZufmsS8jEgqf3zBUWmP6XOf1nihpKWcqvbFUG5H7/JYXA== -"@spotify/eslint-config@^6.1.0": - version "6.1.0" - resolved "https://registry.npmjs.org/@spotify/eslint-config/-/eslint-config-6.1.0.tgz#67ef537c4419ecf275f790a79ce988820b3f5a95" - integrity sha512-pRfRFkW9XMHrzSErzzUxMRs/e+IL0i0o8crgMJJgk0J56+f0rQaXWkeDrI9HCw1uCkXLEwUoNPe+Rdd6cDk/lA== +"@spotify/eslint-config@^7.0.1": + version "7.0.1" + resolved "https://registry.npmjs.org/@spotify/eslint-config/-/eslint-config-7.0.1.tgz#07a21cfd7fce89cfc2c6dd5ea5d747e741201b66" + integrity sha512-8GI/TZGUhS4pr7oipT2MjrZFRgXcKzk9YImEusUdD2f5vlCniRFIBQNrvTMkyjfdQqvIVqJPLcdVPXeAgprsMw== dependencies: - "@spotify/eslint-config-base" "^6.1.0" - "@spotify/eslint-config-react" "^6.0.0" - "@spotify/eslint-config-typescript" "^6.0.0" - "@spotify/web-scripts-utils" "^6.0.0" + "@spotify/eslint-config-base" "^7.0.0" + "@spotify/eslint-config-react" "^7.0.1" + "@spotify/eslint-config-typescript" "^7.0.0" + "@spotify/web-scripts-utils" "^7.0.0" "@typescript-eslint/eslint-plugin" "^2.14.0" "@typescript-eslint/parser" "^2.14.0" eslint-config-prettier "^6.0.0" eslint-plugin-jest "^23.6.0" eslint-plugin-jsx-a11y "^6.2.1" eslint-plugin-react "^7.12.4" - -"@spotify/prettier-config@^6.0.0": - version "6.0.0" - resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-6.0.0.tgz#f3a72cf290af8f5b57e89fc65e6eaff8178a6e83" - integrity sha512-lO/ykZ/GNtzH63mFzs1VeqOVNCZwQJyZm/PBNBZuYk3hnWOfDC8XA5G/D4opV/gxqkED5+Ek7LSAnPcQNv/wUA== + eslint-plugin-react-hooks "^4.0.0" "@spotify/prettier-config@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-7.0.0.tgz#47750979d1282197295108b6958360660a955c16" integrity sha512-lIMcx/2oDqTtW84iHKkRJe+8U6HK6GPwWH5sJp9UEHcDpdXomOQYvwcGXy2I2zwPQQ14gYYE6nEJuSnnYqsYRw== -"@spotify/tsconfig@^6.0.0": - version "6.0.0" - resolved "https://registry.npmjs.org/@spotify/tsconfig/-/tsconfig-6.0.0.tgz#bf9fd0b8188494d87ba6c172f0030376d2aa5dd8" - integrity sha512-B+T6fcDRJg3x1G8wrqUQ+xgu6JYWBwPHeVxKybRGHLt4ciDmzJUvtWthXt8Fg3DqvaKnLnfXLcGOp8TiG+f0kA== +"@spotify/tsconfig@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/@spotify/tsconfig/-/tsconfig-7.0.0.tgz#41c402f4eb6d3147bc18427a35205151cbb32cd5" + integrity sha512-MeRFUPMXWBSm6yaUWiESaQsF9B+9Rn1F/w5hbHHzcunc45teXBcgsOrJu1uDOEkhP/9lP0fefuodpP+TYWM1LQ== -"@spotify/web-scripts-utils@^6.0.0": - version "6.0.0" - resolved "https://registry.npmjs.org/@spotify/web-scripts-utils/-/web-scripts-utils-6.0.0.tgz#64642e79a894058510b0b4a239b117323080e853" - integrity sha512-o3htse1lyhLCabmYoQcI2qCsYV0PW6dpnYHhKAF/iycIglzz4/tXOk3EA/MQVlo6bo7crdr++3OeU4Dyk2CsWw== +"@spotify/web-scripts-utils@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/@spotify/web-scripts-utils/-/web-scripts-utils-7.0.0.tgz#8c6b8039fc645a36ac48629eb9ba06600f4d828a" + integrity sha512-McMy0j60lxOHjgDjegthZqEWN/PabphiM30A/mI/Y7xh9+JFnYWBTSm/wgn6EW2BsPpV631xSYYkk6B1Ph14Fw== dependencies: glob "^7.1.4" read-pkg-up "^7.0.1" -"@spotify/web-scripts@^6.0.0": - version "6.1.0" - resolved "https://registry.npmjs.org/@spotify/web-scripts/-/web-scripts-6.1.0.tgz#3fb52aa9c77c4724e056a33e5b5994b327594278" - integrity sha512-1QVbd7HtIlYLIuNwR7Er/S9p3HLmXOFZAwmRLQ0xe67G1/gqIQOXpuMuembWLyTqi15efMrpNBlmvH64eW7nsw== - dependencies: - "@commitlint/cli" "^8.3.3" - "@commitlint/config-conventional" "^8.3.3" - "@spotify/eslint-config" "^6.1.0" - "@spotify/prettier-config" "^6.0.0" - "@spotify/tsconfig" "^6.0.0" - "@spotify/web-scripts-utils" "^6.0.0" - "@types/cross-spawn" "^6.0.0" - "@types/debug" "^4.1.2" - "@types/jest" "^25.1.0" - "@types/react" "^16.8.19" - "@types/react-dom" "^16.8.4" - commander "^4.0.1" - commitizen "^4.0.3" - cross-spawn-promise "^0.10.1" - cz-conventional-changelog "^3.0.2" - debug "^4.1.1" - eslint "^6.8.0" - jest "^25.1.0" - jest-config "^25.1.0" - jest-junit "^10.0.0" - lint-staged "^10.0.4" - prettier "^1.18.2" - semantic-release "^17.0.1" - ts-jest "^25.2.1" - typescript "^3.7.4" - "@storybook/addon-actions@^5.3.17": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-5.3.18.tgz#e3e3b1475cebc9bdd2d563822fba9ac662b2601a" @@ -3832,27 +3369,11 @@ "@theme-ui/core" "^0.3.1" "@theme-ui/mdx" "^0.3.0" -"@tootallnate/once@1": - version "1.0.0" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.0.0.tgz#9c13c2574c92d4503b005feca8f2e16cc1611506" - integrity sha512-KYyTT/T6ALPkIRd2Ge080X/BsXvy9O0hcWTtMWkPvwAwF99+vn6Dv4GzrFT/Nn1LePr+FFDbRXXlqmsy9lw2zA== - "@types/anymatch@*": version "1.3.1" resolved "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== -"@types/babel__core@^7.1.0": - version "7.1.6" - resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.6.tgz#16ff42a5ae203c9af1c6e190ed1f30f83207b610" - integrity sha512-tTnhWszAqvXnhW7m5jQU9PomXSiKXk2sFxpahXvI20SZKu9ylPi8WtIxueZ6ehDWikPT0jeFujMj3X4ZHuf3Tg== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - "@types/babel__core@^7.1.7": version "7.1.7" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89" @@ -3973,13 +3494,6 @@ dependencies: "@types/express" "*" -"@types/cross-spawn@^6.0.0": - version "6.0.1" - resolved "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.1.tgz#60fa0c87046347c17d9735e5289e72b804ca9b63" - integrity sha512-MtN1pDYdI6D6QFDzy39Q+6c9rl2o/xN7aWGe6oZuzqq5N6+YuwFsWiEAv3dNzvzN9YzU+itpN8lBzFpphQKLAw== - dependencies: - "@types/node" "*" - "@types/cssnano@*": version "4.0.0" resolved "https://registry.npmjs.org/@types/cssnano/-/cssnano-4.0.0.tgz#f1bb29d6d0813861a3d87e02946b2988d0110d4e" @@ -3992,11 +3506,6 @@ resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-1.2.1.tgz#c28803ea36fe29788db69efa0ad6c2dc09544e83" integrity sha512-jqK+I36uz4kTBjyk39meed5y31Ab+tXYN/x1dn3nZEus9yOHCLc+VrcIYLc/aSQ0Y7tMPRlIhLetulME76EiiA== -"@types/debug@^4.1.2": - version "4.1.5" - resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" - integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== - "@types/diff@^4.0.2": version "4.0.2" resolved "https://registry.npmjs.org/@types/diff/-/diff-4.0.2.tgz#2e9bb89f9acc3ab0108f0f3dc4dbdcf2fff8a99c" @@ -4165,7 +3674,7 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^25.1.0": +"@types/jest@*": version "25.2.1" resolved "https://registry.npmjs.org/@types/jest/-/jest-25.2.1.tgz#9544cd438607955381c1bdbdb97767a249297db5" integrity sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA== @@ -4350,7 +3859,7 @@ "@types/webpack" "*" "@types/webpack-dev-server" "*" -"@types/react-dom@*", "@types/react-dom@^16.8.4": +"@types/react-dom@*": version "16.9.5" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz#5de610b04a35d07ffd8f44edad93a71032d9aaa7" integrity sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg== @@ -4417,7 +3926,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.8.19", "@types/react@^16.9": +"@types/react@*", "@types/react@^16.9": version "16.9.25" resolved "https://registry.npmjs.org/@types/react/-/react-16.9.25.tgz#6ae2159b40138c792058a23c3c04fd3db49e929e" integrity sha512-Dlj2V72cfYLPNscIG3/SMUOzhzj7GK3bpSrfefwt2YT9GLynvLCCZjbhyF6VsT0q0+aRACRX03TDJGb7cA0cqg== @@ -4444,11 +3953,6 @@ dependencies: "@types/node" "*" -"@types/retry@^0.12.0": - version "0.12.0" - resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - "@types/rollup-plugin-peer-deps-external@^2.2.0": version "2.2.0" resolved "https://registry.npmjs.org/@types/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.0.tgz#eae7d8b9d27fa037f5bcaded24e389f85b81973c" @@ -4897,7 +4401,7 @@ mkdirp-promise "^5.0.1" mz "^2.5.0" -JSONStream@^1.0.4, JSONStream@^1.3.4, JSONStream@^1.3.5: +JSONStream@^1.0.4, JSONStream@^1.3.4: version "1.3.5" resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== @@ -4910,7 +4414,7 @@ abab@^2.0.0, abab@^2.0.3: resolved "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== -abbrev@1, abbrev@~1.1.1: +abbrev@1: version "1.1.1" resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== @@ -4923,7 +4427,7 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-globals@^4.1.0, acorn-globals@^4.3.2: +acorn-globals@^4.1.0: version "4.3.4" resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== @@ -4964,7 +4468,7 @@ acorn@^6.0.1, acorn@^6.4.1: resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^7.1.0, acorn@^7.1.1: +acorn@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== @@ -4981,13 +4485,6 @@ agent-base@4, agent-base@^4.3.0: dependencies: es6-promisify "^5.0.0" -agent-base@6: - version "6.0.0" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz#5d0101f19bbfaed39980b22ae866de153b93f09a" - integrity sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw== - dependencies: - debug "4" - agent-base@~4.2.1: version "4.2.1" resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" @@ -5058,13 +4555,6 @@ alphanum-sort@^1.0.0: resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= -ansi-align@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" - integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38= - dependencies: - string-width "^2.0.0" - ansi-align@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" @@ -5082,7 +4572,7 @@ ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== -ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: +ansi-escapes@^4.2.1: version "4.3.1" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== @@ -5141,16 +4631,6 @@ ansi-to-html@^0.6.11: dependencies: entities "^1.1.2" -ansicolors@~0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" - integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= - -ansistyles@~0.1.3: - version "0.1.3" - resolved "https://registry.npmjs.org/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" - integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk= - any-observable@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" @@ -5221,12 +4701,12 @@ app-root-dir@^1.0.2: resolved "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz#38187ec2dea7577fff033ffcb12172692ff6e118" integrity sha1-OBh+wt6nV3//Az/8sSFyaS/24Rg= -aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2: +aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -"aproba@^1.1.2 || 2", aproba@^2.0.0: +aproba@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== @@ -5236,11 +4716,6 @@ arch@2.1.1: resolved "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== -archy@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" - integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= - are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" @@ -5261,11 +4736,6 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -argv-formatter@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/argv-formatter/-/argv-formatter-1.0.0.tgz#a0ca0cbc29a5b73e836eebe1cbf6c5e0e4eb82f9" - integrity sha1-oMoMvCmltz6Dbuvhy/bF4OTrgvk= - aria-query@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" @@ -5580,19 +5050,6 @@ babel-helper-to-multiple-sequence-expressions@^0.5.0: resolved "https://registry.npmjs.org/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.5.0.tgz#a3f924e3561882d42fcf48907aa98f7979a4588d" integrity sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA== -babel-jest@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-25.1.0.tgz#206093ac380a4b78c4404a05b3277391278f80fb" - integrity sha512-tz0VxUhhOE2y+g8R2oFrO/2VtVjA1lkJeavlhExuRBg3LdNJY9gwQ+Vcvqt9+cqy71MCTJhewvTB7Qtnnr9SWg== - dependencies: - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" - "@types/babel__core" "^7.1.0" - babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^25.1.0" - chalk "^3.0.0" - slash "^3.0.0" - babel-jest@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-26.0.1.tgz#450139ce4b6c17174b136425bda91885c397bc46" @@ -5646,13 +5103,6 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.1.0.tgz#fb62d7b3b53eb36c97d1bc7fec2072f9bd115981" - integrity sha512-oIsopO41vW4YFZ9yNYoLQATnnN46lp+MZ6H4VvPKFkcc2/fkl3CfE/NZZSmnEIEsJRmJAgkVEK0R7Zbl50CpTw== - dependencies: - "@types/babel__traverse" "^7.0.6" - babel-plugin-jest-hoist@^26.0.0: version "26.0.0" resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.0.0.tgz#fd1d35f95cf8849fc65cb01b5e58aedd710b34a8" @@ -5825,15 +5275,6 @@ babel-plugin-transform-undefined-to-void@^6.9.4: resolved "https://registry.npmjs.org/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.9.4.tgz#be241ca81404030678b748717322b89d0c8fe280" integrity sha1-viQcqBQEAwZ4t0hxcyK4nQyP4oA= -babel-polyfill@6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" - integrity sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM= - dependencies: - babel-runtime "^6.26.0" - core-js "^2.5.0" - regenerator-runtime "^0.10.5" - babel-preset-current-node-syntax@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.2.tgz#fb4a4c51fe38ca60fede1dc74ab35eb843cb41d6" @@ -5850,15 +5291,6 @@ babel-preset-current-node-syntax@^0.1.2: "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -babel-preset-jest@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.1.0.tgz#d0aebfebb2177a21cde710996fce8486d34f1d33" - integrity sha512-eCGn64olaqwUMaugXsTtGAM2I0QTahjEtnRu0ql8Ie+gDWAc1N6wqN0k2NilnyTunM69Pad7gJY7LOtwLimoFQ== - dependencies: - "@babel/plugin-syntax-bigint" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - babel-plugin-jest-hoist "^25.1.0" - babel-preset-jest@^26.0.0: version "26.0.0" resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.0.0.tgz#1eac82f513ad36c4db2e9263d7c485c825b1faa6" @@ -5896,7 +5328,7 @@ babel-preset-jest@^26.0.0: babel-plugin-transform-undefined-to-void "^6.9.4" lodash "^4.17.11" -babel-runtime@6.26.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0: +babel-runtime@6.26.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -5961,7 +5393,7 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: dependencies: tweetnacl "^0.14.3" -before-after-hook@^2.0.0, before-after-hook@^2.1.0: +before-after-hook@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== @@ -5981,18 +5413,6 @@ big.js@^5.2.2: resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -bin-links@^1.1.2, bin-links@^1.1.7: - version "1.1.7" - resolved "https://registry.npmjs.org/bin-links/-/bin-links-1.1.7.tgz#34b79ea9d0e575d7308afeff0c6b2fc24c793359" - integrity sha512-/eaLaTu7G7/o7PV04QPy1HRT65zf+1tFkPGv0sPTV0tRwufooYBQO3zrcyGgm+ja+ZtBf2GEuKjDRJ2pPG+yqA== - dependencies: - bluebird "^3.5.3" - cmd-shim "^3.0.0" - gentle-fs "^2.3.0" - graceful-fs "^4.1.15" - npm-normalize-package-bin "^1.0.0" - write-file-atomic "^2.3.0" - binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -6062,11 +5482,6 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -bottleneck@^2.18.1: - version "2.19.5" - resolved "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" - integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== - bowser@2.9.0: version "2.9.0" resolved "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9" @@ -6077,19 +5492,6 @@ bowser@^1.7.3: resolved "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== -boxen@^1.2.1: - version "1.3.0" - resolved "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" - integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw== - dependencies: - ansi-align "^2.0.0" - camelcase "^4.0.0" - chalk "^2.0.1" - cli-boxes "^1.0.0" - string-width "^2.0.0" - term-size "^1.2.0" - widest-line "^2.0.0" - boxen@^4.1.0, boxen@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" @@ -6145,13 +5547,6 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browser-resolve@^1.11.3: - version "1.11.3" - resolved "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" - integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== - dependencies: - resolve "1.1.7" - browserify-aes@^1.0.0, browserify-aes@^1.0.4: version "1.2.0" resolved "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" @@ -6394,21 +5789,11 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -cachedir@2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.2.0.tgz#19afa4305e05d79e417566882e0c8f960f62ff0e" - integrity sha512-VvxA0xhNqIIfg0V9AmJkDg91DaJwryutH5rVEZAhcNi4iJFj9f+QxmAjgK1LT9I8OgToX27fypX6/MeCXVbBjQ== - cachedir@2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== -call-limit@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/call-limit/-/call-limit-1.1.1.tgz#ef15f2670db3f1992557e2d965abc459e6e358d4" - integrity sha512-5twvci5b9eRBw2wCfPtN0GmlR2/gadZqyFpPhOK6CvMFoFgA+USnZ6Jpu1lhG9h85pQ3Ouil3PfXWRD4EUaRiQ== - call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -6468,7 +5853,7 @@ camelcase@^2.0.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= -camelcase@^4.0.0, camelcase@^4.1.0: +camelcase@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= @@ -6515,19 +5900,6 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" -capture-stack-trace@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" - integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw== - -cardinal@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" - integrity sha1-fMEFXYItISlU0HsIXeolHMe8VQU= - dependencies: - ansicolors "~0.3.2" - redeyed "~2.1.0" - case-sensitive-paths-webpack-plugin@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7" @@ -6538,7 +5910,7 @@ caseless@~0.12.0: resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -6643,7 +6015,7 @@ chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.3.1: optionalDependencies: fsevents "~2.1.2" -chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.4: +chownr@^1.1.1, chownr@^1.1.2: version "1.1.4" resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -6660,23 +6032,11 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -ci-info@^1.5.0: - version "1.6.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" - integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== - ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -cidr-regex@^2.0.10: - version "2.0.10" - resolved "https://registry.npmjs.org/cidr-regex/-/cidr-regex-2.0.10.tgz#af13878bd4ad704de77d6dc800799358b3afa70d" - integrity sha512-sB3ogMQXWvreNPbJUZMRApxuRYd+KoIo4RGQ81VatjmMW6WJPo+IJZ2846FGItr9VzKo5w7DXzijPLGtSd0N3Q== - dependencies: - ip-regex "^2.1.0" - cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -6719,24 +6079,11 @@ clean-stack@^2.0.0: resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-boxes@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" - integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM= - cli-boxes@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== -cli-columns@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/cli-columns/-/cli-columns-3.1.2.tgz#6732d972979efc2ae444a1f08e08fa139c96a18e" - integrity sha1-ZzLZcpee/CrkRKHwjgj6E5yWoY4= - dependencies: - string-width "^2.0.0" - strip-ansi "^3.0.1" - cli-cursor@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" @@ -6763,7 +6110,7 @@ cli-spinners@^2.2.0: resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== -cli-table3@0.5.1, cli-table3@^0.5.0, cli-table3@^0.5.1: +cli-table3@0.5.1: version "0.5.1" resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== @@ -6773,13 +6120,6 @@ cli-table3@0.5.1, cli-table3@^0.5.0, cli-table3@^0.5.1: optionalDependencies: colors "^1.1.2" -cli-table@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" - integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM= - dependencies: - colors "1.0.3" - cli-truncate@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" @@ -6802,24 +6142,6 @@ clipboard@^2.0.0: select "^1.1.2" tiny-emitter "^2.0.0" -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - -cliui@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" - integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi "^2.0.0" - cliui@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -6875,14 +6197,6 @@ clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== -cmd-shim@^3.0.0, cmd-shim@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/cmd-shim/-/cmd-shim-3.0.3.tgz#2c35238d3df37d98ecdd7d5f6b8dc6b21cadc7cb" - integrity sha512-DtGg+0xiFhQIntSBRzL2fRQBnmtAVwXIDo4Qq46HPpObYquxMaZS4sb82U9nH91qJrlosC1wa9gwr0QyL/HypA== - dependencies: - graceful-fs "^4.1.2" - mkdirp "~0.5.0" - co@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -6991,11 +6305,6 @@ colornames@^1.1.1: resolved "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96" integrity sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y= -colors@1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= - colors@^1.1.2, colors@^1.2.1: version "1.4.0" resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -7009,7 +6318,7 @@ colorspace@1.1.x: color "3.0.x" text-hex "1.0.x" -columnify@^1.5.4, columnify@~1.5.4: +columnify@^1.5.4: version "1.5.4" resolved "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs= @@ -7049,27 +6358,6 @@ commander@^5.1.0: resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commitizen@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/commitizen/-/commitizen-4.0.3.tgz#c19a4213257d0525b85139e2f36db7cc3b4f6dae" - integrity sha512-lxu0F/Iq4dudoFeIl5pY3h3CQJzkmQuh3ygnaOvqhAD8Wu2pYBI17ofqSuPHNsBTEOh1r1AVa9kR4Hp0FAHKcQ== - dependencies: - cachedir "2.2.0" - cz-conventional-changelog "3.0.1" - dedent "0.7.0" - detect-indent "6.0.0" - find-node-modules "2.0.0" - find-root "1.1.0" - fs-extra "8.1.0" - glob "7.1.4" - inquirer "6.5.0" - is-utf8 "^0.2.1" - lodash "4.17.15" - minimist "1.2.0" - shelljs "0.7.6" - strip-bom "4.0.0" - strip-json-comments "3.0.1" - common-tags@1.8.0: version "1.8.0" resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -7150,7 +6438,7 @@ concat-with-sourcemaps@^1.1.0: dependencies: source-map "^0.6.1" -config-chain@^1.1.11, config-chain@^1.1.12: +config-chain@^1.1.11: version "1.1.12" resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== @@ -7158,18 +6446,6 @@ config-chain@^1.1.11, config-chain@^1.1.12: ini "^1.3.4" proto-list "~1.2.1" -configstore@^3.0.0: - version "3.1.2" - resolved "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" - integrity sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw== - dependencies: - dot-prop "^4.1.0" - graceful-fs "^4.1.2" - make-dir "^1.0.0" - unique-string "^1.0.0" - write-file-atomic "^2.0.0" - xdg-basedir "^3.0.0" - configstore@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" @@ -7192,7 +6468,7 @@ console-browserify@^1.1.0: resolved "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== -console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: +console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= @@ -7224,15 +6500,7 @@ content-type@~1.0.4: resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -conventional-changelog-angular@^1.3.3: - version "1.6.6" - resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz#b27f2b315c16d0a1f23eb181309d0e6a4698ea0f" - integrity sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg== - dependencies: - compare-func "^1.3.1" - q "^1.5.1" - -conventional-changelog-angular@^5.0.0, conventional-changelog-angular@^5.0.3: +conventional-changelog-angular@^5.0.3: version "5.0.6" resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.6.tgz#269540c624553aded809c29a3508fdc2b544c059" integrity sha512-QDEmLa+7qdhVIv8sFZfVxU1VSyVvnXPsxq8Vam49mKUcO1Z8VTLEJk9uI21uiJUsnmm0I4Hrsdc9TgkOQo9WSA== @@ -7240,15 +6508,6 @@ conventional-changelog-angular@^5.0.0, conventional-changelog-angular@^5.0.3: compare-func "^1.3.1" q "^1.5.1" -conventional-changelog-conventionalcommits@4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.2.1.tgz#d6cb2e2c5d7bfca044a08b9dba84b4082e1a1bd9" - integrity sha512-vC02KucnkNNap+foDKFm7BVUSDAXktXrUJqGszUuYnt6T0J2azsbYz/w9TDc3VsrW2v6JOtiQWVcgZnporHr4Q== - dependencies: - compare-func "^1.3.1" - lodash "^4.2.1" - q "^1.5.1" - conventional-changelog-core@^3.1.6: version "3.2.3" resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-3.2.3.tgz#b31410856f431c847086a7dcb4d2ca184a7d88fb" @@ -7273,7 +6532,7 @@ conventional-changelog-preset-loader@^2.1.1: resolved "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.0.tgz#580fa8ab02cef22c24294d25e52d7ccd247a9a6a" integrity sha512-/rHb32J2EJnEXeK4NpDgMaAVTFZS3o1ExmjKMtYVgIC4MQn0vkNSbYpdGRotkfGGRWiqk3Ri3FBkiZGbAfIfOQ== -conventional-changelog-writer@^4.0.0, conventional-changelog-writer@^4.0.6: +conventional-changelog-writer@^4.0.6: version "4.0.11" resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.11.tgz#9f56d2122d20c96eb48baae0bf1deffaed1edba4" integrity sha512-g81GQOR392I+57Cw3IyP1f+f42ME6aEkbR+L7v1FBBWolB0xkjKTeCWVguzRrp6UiT1O6gBpJbEy2eq7AnV1rw== @@ -7289,17 +6548,7 @@ conventional-changelog-writer@^4.0.0, conventional-changelog-writer@^4.0.6: split "^1.0.0" through2 "^3.0.0" -conventional-commit-types@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-2.3.0.tgz#bc3c8ebba0a9e4b3ecc548f1d0674e251ab8be22" - integrity sha512-6iB39PrcGYdz0n3z31kj6/Km6mK9hm9oMRhwcLnKxE7WNoeRKZbTAobliKrbYZ5jqyCvtcVEfjCiaEzhL3AVmQ== - -conventional-commit-types@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-3.0.0.tgz#7c9214e58eae93e85dd66dbfbafe7e4fffa2365b" - integrity sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg== - -conventional-commits-filter@^2.0.0, conventional-commits-filter@^2.0.2: +conventional-commits-filter@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz#f122f89fbcd5bb81e2af2fcac0254d062d1039c1" integrity sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ== @@ -7307,7 +6556,7 @@ conventional-commits-filter@^2.0.0, conventional-commits-filter@^2.0.2: lodash.ismatch "^4.4.0" modify-values "^1.0.0" -conventional-commits-parser@^3.0.0, conventional-commits-parser@^3.0.3, conventional-commits-parser@^3.0.7: +conventional-commits-parser@^3.0.3: version "3.0.8" resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.8.tgz#23310a9bda6c93c874224375e72b09fb275fe710" integrity sha512-YcBSGkZbYp7d+Cr3NWUeXbPDFUN6g3SaSIzOybi8bjHL5IJ5225OSCxJJ4LgziyEJ7AaJtE9L2/EU6H7Nt/DDQ== @@ -7401,7 +6650,7 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1: resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a" integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw== -core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.5: +core-js@^2.4.0, core-js@^2.6.5: version "2.6.11" resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== @@ -7443,7 +6692,7 @@ cosmiconfig@6.0.0, cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" -cosmiconfig@^5.0.0, cosmiconfig@^5.1.0, cosmiconfig@^5.2.0, cosmiconfig@^5.2.1: +cosmiconfig@^5.0.0, cosmiconfig@^5.1.0, cosmiconfig@^5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== @@ -7461,13 +6710,6 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-error-class@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" - integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y= - dependencies: - capture-stack-trace "^1.0.0" - create-hash@^1.1.0, create-hash@^1.1.2: version "1.2.0" resolved "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" @@ -7514,14 +6756,7 @@ cross-fetch@3.0.4, cross-fetch@^3.0.4: node-fetch "2.6.0" whatwg-fetch "3.0.0" -cross-spawn-promise@^0.10.1: - version "0.10.2" - resolved "https://registry.npmjs.org/cross-spawn-promise/-/cross-spawn-promise-0.10.2.tgz#0e6338149caf53a6d557ac5c65efb3086d8704ac" - integrity sha512-74PXJf6DYaab2klRS+D+9qxKJL1Weo3/ao9OPoH6NFzxtINSa/HE2mcyAPu1fpEmRTPD4Gdmpg3xEXQSgI8lpg== - dependencies: - cross-spawn "^5.1.0" - -cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@6.0.5, cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -7541,7 +6776,7 @@ cross-spawn@7.0.1, cross-spawn@^7.0.0, cross-spawn@^7.0.1: shebang-command "^2.0.0" which "^2.0.1" -cross-spawn@^5.0.1, cross-spawn@^5.1.0: +cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= @@ -7550,6 +6785,15 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -7567,11 +6811,6 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" -crypto-random-string@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" - integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= - crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" @@ -7822,7 +7061,7 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@~0.3.6: resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== -cssom@^0.4.1, cssom@^0.4.4: +cssom@^0.4.4: version "0.4.4" resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== @@ -7834,13 +7073,6 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" -cssstyle@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.2.0.tgz#e4c44debccd6b7911ed617a4395e5754bba59992" - integrity sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA== - dependencies: - cssom "~0.3.6" - cssstyle@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" @@ -7908,35 +7140,6 @@ cypress@*, cypress@^4.2.0: url "0.11.0" yauzl "2.10.0" -cz-conventional-changelog@3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.0.1.tgz#b1f207ae050355e7ada65aad5c52e9de3d0c8e5b" - integrity sha512-7KASIwB8/ClEyCRvQrCPbN7WkQnUSjSSVNyPM+gDJ0jskLi8h8N2hrdpyeCk7fIqKMRzziqVSOBTB8yyLTMHGQ== - dependencies: - chalk "^2.4.1" - conventional-commit-types "^2.0.0" - lodash.map "^4.5.1" - longest "^2.0.1" - right-pad "^1.0.1" - word-wrap "^1.0.3" - optionalDependencies: - "@commitlint/load" ">6.1.1" - -cz-conventional-changelog@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.1.0.tgz#1e004a4f507531347a5f78ab4ed65c3ff693fc97" - integrity sha512-SCwPPOF+7qMh1DZkJhrwaxCvZzPaz2E9BwQzcZwBuHlpcJj9zzz7K5vADQRhHuxStaHZFSLbDlZEdcls4bKu7Q== - dependencies: - chalk "^2.4.1" - commitizen "^4.0.3" - conventional-commit-types "^3.0.0" - lodash.map "^4.5.1" - longest "^2.0.1" - right-pad "^1.0.1" - word-wrap "^1.0.3" - optionalDependencies: - "@commitlint/load" ">6.1.1" - d3-dispatch@1: version "1.0.6" resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" @@ -7990,7 +7193,7 @@ dashify@^2.0.0: resolved "https://registry.npmjs.org/dashify/-/dashify-2.0.0.tgz#fff270ca2868ca427fee571de35691d6e437a648" integrity sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A== -data-urls@^1.0.0, data-urls@^1.1.0: +data-urls@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== @@ -8042,7 +7245,7 @@ debug@3.1.0, debug@=3.1.0: dependencies: ms "2.0.0" -debug@4, debug@4.1.1, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -8069,7 +7272,7 @@ decamelize-keys@^1.0.0: decamelize "^1.1.0" map-obj "^1.0.0" -decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -8091,7 +7294,7 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" -dedent@0.7.0, dedent@^0.7.0: +dedent@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= @@ -8113,7 +7316,7 @@ deep-extend@^0.6.0: resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@~0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= @@ -8257,12 +7460,7 @@ detect-file@^1.0.0: resolved "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= -detect-indent@6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" - integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== - -detect-indent@^5.0.0, detect-indent@~5.0.0: +detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= @@ -8272,11 +7470,6 @@ detect-libc@^1.0.2: resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= -detect-newline@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" - integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= - detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -8303,7 +7496,7 @@ detect-port@^1.3.0: address "^1.0.1" debug "^2.6.0" -dezalgo@^1.0.0, dezalgo@~1.0.3: +dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= @@ -8359,7 +7552,7 @@ dir-glob@^2.0.0, dir-glob@^2.2.2: dependencies: path-type "^3.0.0" -dir-glob@^3.0.0, dir-glob@^3.0.1: +dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== @@ -8566,7 +7759,7 @@ dot-prop@^3.0.0: dependencies: is-obj "^1.0.0" -dot-prop@^4.1.0, dot-prop@^4.2.0: +dot-prop@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== @@ -8599,11 +7792,6 @@ dotenv-webpack@^1.7.0: dependencies: dotenv-defaults "^1.0.2" -dotenv@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef" - integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow== - dotenv@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064" @@ -8614,13 +7802,6 @@ dotenv@^8.0.0: resolved "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== -duplexer2@~0.1.0: - version "0.1.4" - resolved "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" - integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= - dependencies: - readable-stream "^2.0.2" - duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -8649,11 +7830,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -editor@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742" - integrity sha1-YMf4e9YrzGqJT6jM1q+3gjok90I= - ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -8768,14 +7944,6 @@ entities@^2.0.0, entities@~2.0.0: resolved "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== -env-ci@^5.0.0: - version "5.0.2" - resolved "https://registry.npmjs.org/env-ci/-/env-ci-5.0.2.tgz#48b6687f8af8cdf5e31b8fcf2987553d085249d9" - integrity sha512-Xc41mKvjouTXD3Oy9AqySz1IeyvJvHZ20Twf5ZLYbNpPPIuCnL/qHCmNlD01LoNy0JTunw9HPYVptD19Ac7Mbw== - dependencies: - execa "^4.0.0" - java-properties "^1.0.0" - env-paths@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" @@ -8908,7 +8076,7 @@ escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escodegen@^1.11.1, escodegen@^1.14.1, escodegen@^1.9.1: +escodegen@^1.14.1, escodegen@^1.9.1: version "1.14.1" resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== @@ -9020,6 +8188,11 @@ eslint-plugin-notice@^0.9.10: lodash "^4.17.15" metric-lcs "^0.1.2" +eslint-plugin-react-hooks@^4.0.0: + version "4.0.4" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.4.tgz#aed33b4254a41b045818cacb047b81e6df27fa58" + integrity sha512-equAdEIsUETLFNCmmCkiCGq6rkSK5MoJhXFPFYeUebcjKgBmWWcgVOqZyQC8Bv1BwVCnTq9tBxgJFgAJTWoJtA== + eslint-plugin-react@^7.12.4: version "7.19.0" resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz#6d08f9673628aa69c5559d33489e855d83551666" @@ -9061,27 +8234,34 @@ eslint-utils@^1.4.3: dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" + integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA== + dependencies: + eslint-visitor-keys "^1.1.0" + eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== -eslint@^6.8.0: - version "6.8.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" - integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== +eslint@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-7.1.0.tgz#d9a1df25e5b7859b0a3d86bb05f0940ab676a851" + integrity sha512-DfS3b8iHMK5z/YLSme8K5cge168I8j8o1uiVmFCgnnjxZQbCGyraF8bMl7Ju4yfBmCuxD7shOF7eqGkcuIHfsA== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.10.0" - chalk "^2.1.0" - cross-spawn "^6.0.5" + chalk "^4.0.0" + cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" eslint-scope "^5.0.0" - eslint-utils "^1.4.3" + eslint-utils "^2.0.0" eslint-visitor-keys "^1.1.0" - espree "^6.1.2" - esquery "^1.0.1" + espree "^7.0.0" + esquery "^1.2.0" esutils "^2.0.2" file-entry-cache "^5.0.1" functional-red-black-tree "^1.0.1" @@ -9094,17 +8274,16 @@ eslint@^6.8.0: is-glob "^4.0.0" js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" + levn "^0.4.1" lodash "^4.17.14" minimatch "^3.0.4" - mkdirp "^0.5.1" natural-compare "^1.4.0" - optionator "^0.8.3" + optionator "^0.9.1" progress "^2.0.0" - regexpp "^2.0.1" - semver "^6.1.2" - strip-ansi "^5.2.0" - strip-json-comments "^3.0.1" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" table "^5.2.3" text-table "^0.2.0" v8-compile-cache "^2.0.3" @@ -9114,10 +8293,10 @@ esm@^3.2.25: resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== -espree@^6.1.2: - version "6.2.1" - resolved "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" - integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== +espree@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/espree/-/espree-7.0.0.tgz#8a7a60f218e69f120a842dc24c5a88aa7748a74e" + integrity sha512-/r2XEx5Mw4pgKdyb7GNLQNsu++asx/dltf/CI8RFi9oGHxmQFgvLbc5Op4U6i8Oaj+kdslhJtVlEZeAqH5qOTw== dependencies: acorn "^7.1.1" acorn-jsx "^5.2.0" @@ -9128,12 +8307,12 @@ esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.0.1: - version "1.1.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz#c5c0b66f383e7656404f86b31334d72524eddb48" - integrity sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q== +esquery@^1.2.0: + version "1.3.1" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== dependencies: - estraverse "^4.0.0" + estraverse "^5.1.0" esrecurse@^4.1.0: version "4.2.1" @@ -9142,11 +8321,16 @@ esrecurse@^4.1.0: dependencies: estraverse "^4.1.0" -estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== +estraverse@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== + estree-walker@^0.6.0, estree-walker@^0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" @@ -9233,7 +8417,7 @@ execa@1.0.0, execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@3.4.0, execa@^3.2.0, execa@^3.4.0: +execa@3.4.0, execa@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" integrity sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g== @@ -9249,19 +8433,6 @@ execa@3.4.0, execa@^3.2.0, execa@^3.4.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf" @@ -9319,18 +8490,6 @@ expect-ct@0.2.0: resolved "https://registry.npmjs.org/expect-ct/-/expect-ct-0.2.0.tgz#3a54741b6ed34cc7a93305c605f63cd268a54a62" integrity sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g== -expect@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/expect/-/expect-25.1.0.tgz#7e8d7b06a53f7d66ec927278db3304254ee683ee" - integrity sha512-wqHzuoapQkhc3OKPlrpetsfueuEiMf3iWh0R8+duCu9PIjXoP7HgD5aeypwTnXUAjC8aMsiVDaWwlbJ1RlQ38g== - dependencies: - "@jest/types" "^25.1.0" - ansi-styles "^4.0.0" - jest-get-type "^25.1.0" - jest-matcher-utils "^25.1.0" - jest-message-util "^25.1.0" - jest-regex-util "^25.1.0" - expect@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/expect/-/expect-26.0.1.tgz#18697b9611a7e2725e20ba3ceadda49bc9865421" @@ -9696,20 +8855,7 @@ find-cache-dir@^3.0.0, find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-node-modules@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.0.0.tgz#5db1fb9e668a3d451db3d618cd167cdd59e41b69" - integrity sha512-8MWIBRgJi/WpjjfVXumjPKCtmQ10B+fjx6zmSA+770GMJirLhWIzg8l763rhjl9xaeaHbnxPNRQKq2mgMhr+aw== - dependencies: - findup-sync "^3.0.0" - merge "^1.2.1" - -find-npm-prefix@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/find-npm-prefix/-/find-npm-prefix-1.0.2.tgz#8d8ce2c78b3b4b9e66c8acc6a37c231eb841cfdf" - integrity sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA== - -find-root@1.1.0, find-root@^1.1.0: +find-root@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== @@ -9744,7 +8890,7 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" -find-versions@^3.0.0, find-versions@^3.2.0: +find-versions@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== @@ -9787,9 +8933,9 @@ flat-cache@^2.0.1: write "1.0.3" flatted@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" - integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== + version "2.0.2" + resolved "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== flush-write-stream@^1.0.0: version "1.1.1" @@ -9952,15 +9098,7 @@ fresh@0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -from2@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/from2/-/from2-1.3.0.tgz#88413baaa5f9a597cfde9221d86986cd3c061dfd" - integrity sha1-iEE7qqX5pZfP3pIh2GmGzTwGHf0= - dependencies: - inherits "~2.0.1" - readable-stream "~1.1.10" - -from2@^2.1.0, from2@^2.3.0: +from2@^2.1.0: version "2.3.0" resolved "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= @@ -10022,16 +9160,7 @@ fs-minipass@^2.0.0: dependencies: minipass "^3.0.0" -fs-vacuum@^1.2.10, fs-vacuum@~1.2.10: - version "1.2.10" - resolved "https://registry.npmjs.org/fs-vacuum/-/fs-vacuum-1.2.10.tgz#b7629bec07a4031a2548fdf99f5ecf1cc8b31e36" - integrity sha1-t2Kb7AekAxolSP35n17PHMizHjY= - dependencies: - graceful-fs "^4.1.2" - path-is-inside "^1.0.1" - rimraf "^2.5.2" - -fs-write-stream-atomic@^1.0.8, fs-write-stream-atomic@~1.0.10: +fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= @@ -10119,28 +9248,6 @@ gensync@^1.0.0-beta.1: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -gentle-fs@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/gentle-fs/-/gentle-fs-2.3.0.tgz#13538db5029400f98684be4894e8a7d8f0d1ea7f" - integrity sha512-3k2CgAmPxuz7S6nKK+AqFE2AdM1QuwqKLPKzIET3VRwK++3q96MsNFobScDjlCrq97ZJ8y5R725MOlm6ffUCjg== - dependencies: - aproba "^1.1.2" - chownr "^1.1.2" - cmd-shim "^3.0.3" - fs-vacuum "^1.2.10" - graceful-fs "^4.1.11" - iferr "^0.1.5" - infer-owner "^1.0.4" - mkdirp "^0.5.1" - path-is-inside "^1.0.2" - read-cmd-shim "^1.0.1" - slide "^1.1.6" - -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -10180,11 +9287,6 @@ get-port@^5.1.1: resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== -get-stdin@7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" - integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ== - get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -10195,11 +9297,6 @@ get-stdin@^6.0.0: resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -10238,18 +9335,6 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -git-log-parser@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.0.tgz#2e6a4c1b13fc00028207ba795a7ac31667b9fd4a" - integrity sha1-LmpMGxP8AAKCB7p5WnrDFme5/Uo= - dependencies: - argv-formatter "~1.0.0" - spawn-error-forwarder "~1.0.0" - split2 "~1.0.0" - stream-combiner2 "~1.1.1" - through2 "~2.0.0" - traverse "~0.6.6" - git-raw-commits@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" @@ -10261,17 +9346,6 @@ git-raw-commits@2.0.0: split2 "^2.0.0" through2 "^2.0.0" -git-raw-commits@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.3.tgz#f040e67b8445962d4d168903a9e84c4240c17655" - integrity sha512-SoSsFL5lnixVzctGEi2uykjA7B5I0AhO9x6kdzvGRHbxsa6JSEgrgy1esRKsfOKE1cgyOJ/KDR2Trxu157sb8w== - dependencies: - dargs "^4.0.1" - lodash.template "^4.0.2" - meow "^5.0.0" - split2 "^2.0.0" - through2 "^3.0.0" - git-remote-origin-url@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" @@ -10345,18 +9419,6 @@ glob-to-regexp@^0.3.0: resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@7.1.4: - version "7.1.4" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -10369,7 +9431,7 @@ glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glo once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^0.1.0, global-dirs@^0.1.1: +global-dirs@^0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= @@ -10529,23 +9591,6 @@ good-listener@^1.2.2: dependencies: delegate "^3.1.2" -got@^6.7.1: - version "6.7.1" - resolved "https://registry.npmjs.org/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" - integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA= - dependencies: - create-error-class "^3.0.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - is-redirect "^1.0.0" - is-retry-allowed "^1.0.0" - is-stream "^1.0.0" - lowercase-keys "^1.0.0" - safe-buffer "^5.0.1" - timed-out "^4.0.0" - unzip-response "^2.0.1" - url-parse-lax "^1.0.0" - got@^9.6.0: version "9.6.0" resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -10563,7 +9608,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.3" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== @@ -10740,7 +9785,7 @@ has-symbols@^1.0.0, has-symbols@^1.0.1: resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== -has-unicode@^2.0.0, has-unicode@^2.0.1, has-unicode@~2.0.1: +has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= @@ -10915,28 +9960,16 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" -hook-std@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/hook-std/-/hook-std-2.0.0.tgz#ff9aafdebb6a989a354f729bb6445cf4a3a7077c" - integrity sha512-zZ6T5WcuBMIUVh49iPQS9t977t7C0l7OtHrpeMb5uk48JdflRX0NSFvCekfYNmGQETnLq9W/isMyHl69kxGi8g== - hoopy@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== -hosted-git-info@^2.1.4, hosted-git-info@^2.7.1, hosted-git-info@^2.8.8: +hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: version "2.8.8" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== -hosted-git-info@^3.0.0: - version "3.0.4" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz#be4973eb1fd2737b11c9c7c19380739bb249f60d" - integrity sha512-4oT62d2jwSDBbLLFLZE+1vPuQ1h8p9wjrJ8Mqx5TjsyWmBMV5B13eJqn8pvluqubLf3cJPTfiYCIwNwDNmzScQ== - dependencies: - lru-cache "^5.1.1" - hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -11118,15 +10151,6 @@ http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" -http-proxy-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - http-proxy-middleware@0.19.1: version "0.19.1" resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" @@ -11168,14 +10192,6 @@ https-proxy-agent@^2.2.3: agent-base "^4.3.0" debug "^3.1.0" -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - human-signals@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -11250,11 +10266,6 @@ iferr@^0.1.5: resolved "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= -iferr@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/iferr/-/iferr-1.0.2.tgz#e9fde49a9da06dc4a4194c6c9ed6d08305037a6d" - integrity sha512-9AfeLfji44r5TKInjhz3W9DyZI1zR1JAf2hVBMGhddAKPqBsupb89jGfbCTHIGZd6fGZl9WlHdn4AObygyMKwg== - ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" @@ -11389,7 +10400,7 @@ infer-owner@^1.0.3, infer-owner@^1.0.4: resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== -inflight@^1.0.4, inflight@~1.0.6: +inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= @@ -11542,14 +10553,6 @@ interpret@^2.0.0: resolved "https://registry.npmjs.org/interpret/-/interpret-2.0.0.tgz#b783ffac0b8371503e9ab39561df223286aa5433" integrity sha512-e0/LknJ8wpMMhTiWcjivB+ESwIuvHnBSlBbmP/pSb8CQJldoj1p2qv7xGZ/+BtbTziYRFSz8OsvdbiX45LtYQA== -into-stream@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/into-stream/-/into-stream-5.1.1.tgz#f9a20a348a11f3c13face22763f2d02e127f4db8" - integrity sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA== - dependencies: - from2 "^2.3.0" - p-is-promise "^3.0.0" - invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4: version "2.2.4" resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -11557,16 +10560,6 @@ invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== - ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -11673,20 +10666,6 @@ is-ci@2.0.0, is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-ci@^1.0.10: - version "1.2.1" - resolved "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" - integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== - dependencies: - ci-info "^1.5.0" - -is-cidr@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/is-cidr/-/is-cidr-3.1.0.tgz#72e233d8e1c4cd1d3f11713fcce3eba7b0e3476f" - integrity sha512-3kxTForpuj8O4iHn0ocsn1jxRm5VYm60GDghK6HXmpn4IyZOoRy9/GmdjFA2yEMqw91TB1/K3bFTuI7FlFNR1g== - dependencies: - cidr-regex "^2.0.10" - is-color-stop@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" @@ -11844,7 +10823,7 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3: resolved "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= -is-installed-globally@0.1.0, is-installed-globally@^0.1.0: +is-installed-globally@0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= @@ -11875,11 +10854,6 @@ is-module@^1.0.0: resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= -is-npm@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" - integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= - is-npm@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" @@ -11979,11 +10953,6 @@ is-promise@^2.1.0: resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= -is-redirect@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" - integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= - is-reference@^1.1.2: version "1.1.4" resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz#3f95849886ddb70256a3e6d062b1a68c13c51427" @@ -12015,11 +10984,6 @@ is-resolvable@^1.0.0: resolved "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== -is-retry-allowed@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" - integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== - is-root@2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" @@ -12037,7 +11001,7 @@ is-ssh@^1.3.0: dependencies: protocols "^1.1.0" -is-stream@^1.0.0, is-stream@^1.1.0: +is-stream@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -12085,7 +11049,7 @@ is-unc-path@^1.0.0: dependencies: unc-path-regex "^0.1.2" -is-utf8@^0.2.0, is-utf8@^0.2.1: +is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= @@ -12167,17 +11131,6 @@ isstream@~0.1.2: resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -issue-parser@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz#b1edd06315d4f2044a9755daf85fdafde9b4014a" - integrity sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA== - dependencies: - lodash.capitalize "^4.2.1" - lodash.escaperegexp "^4.1.2" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.uniqby "^4.7.0" - istanbul-lib-coverage@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" @@ -12214,14 +11167,6 @@ istanbul-lib-source-maps@^4.0.0: istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.0.tgz#d4d16d035db99581b6194e119bbf36c963c5eb70" - integrity sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - istanbul-reports@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" @@ -12248,20 +11193,6 @@ iterate-value@^1.0.0: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" -java-properties@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211" - integrity sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ== - -jest-changed-files@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.1.0.tgz#73dae9a7d9949fdfa5c278438ce8f2ff3ec78131" - integrity sha512-bdL1aHjIVy3HaBO3eEQeemGttsq1BDlHgWcOjEOIAcga7OOEGWHD2WSu8HhL7I1F0mFFyci8VKU4tRNk+qtwDA== - dependencies: - "@jest/types" "^25.1.0" - execa "^3.2.0" - throat "^5.0.0" - jest-changed-files@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.0.1.tgz#1334630c6a1ad75784120f39c3aa9278e59f349f" @@ -12271,25 +11202,6 @@ jest-changed-files@^26.0.1: execa "^4.0.0" throat "^5.0.0" -jest-cli@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-25.1.0.tgz#75f0b09cf6c4f39360906bf78d580be1048e4372" - integrity sha512-p+aOfczzzKdo3AsLJlhs8J5EW6ffVidfSZZxXedJ0mHPBOln1DccqFmGCoO8JWd4xRycfmwy1eoQkMsF8oekPg== - dependencies: - "@jest/core" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" - chalk "^3.0.0" - exit "^0.1.2" - import-local "^3.0.2" - is-ci "^2.0.0" - jest-config "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" - prompts "^2.0.1" - realpath-native "^1.1.0" - yargs "^15.0.0" - jest-cli@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-26.0.1.tgz#3a42399a4cbc96a519b99ad069a117d955570cac" @@ -12309,29 +11221,6 @@ jest-cli@^26.0.1: prompts "^2.0.1" yargs "^15.3.1" -jest-config@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-config/-/jest-config-25.1.0.tgz#d114e4778c045d3ef239452213b7ad3ec1cbea90" - integrity sha512-tLmsg4SZ5H7tuhBC5bOja0HEblM0coS3Wy5LTCb2C8ZV6eWLewHyK+3qSq9Bi29zmWQ7ojdCd3pxpx4l4d2uGw== - dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^25.1.0" - "@jest/types" "^25.1.0" - babel-jest "^25.1.0" - chalk "^3.0.0" - glob "^7.1.1" - jest-environment-jsdom "^25.1.0" - jest-environment-node "^25.1.0" - jest-get-type "^25.1.0" - jest-jasmine2 "^25.1.0" - jest-regex-util "^25.1.0" - jest-resolve "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" - micromatch "^4.0.2" - pretty-format "^25.1.0" - realpath-native "^1.1.0" - jest-config@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-config/-/jest-config-26.0.1.tgz#096a3d4150afadf719d1fab00e9a6fb2d6d67507" @@ -12383,13 +11272,6 @@ jest-diff@^26.0.1: jest-get-type "^26.0.0" pretty-format "^26.0.1" -jest-docblock@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.1.0.tgz#0f44bea3d6ca6dfc38373d465b347c8818eccb64" - integrity sha512-370P/mh1wzoef6hUKiaMcsPtIapY25suP6JqM70V9RJvdKLrV4GaGbfUseUVk4FZJw4oTZ1qSCJNdrClKt5JQA== - dependencies: - detect-newline "^3.0.0" - jest-docblock@^26.0.0: version "26.0.0" resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" @@ -12397,17 +11279,6 @@ jest-docblock@^26.0.0: dependencies: detect-newline "^3.0.0" -jest-each@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-each/-/jest-each-25.1.0.tgz#a6b260992bdf451c2d64a0ccbb3ac25e9b44c26a" - integrity sha512-R9EL8xWzoPySJ5wa0DXFTj7NrzKpRD40Jy+zQDp3Qr/2QmevJgkN9GqioCGtAJ2bW9P/MQRznQHQQhoeAyra7A== - dependencies: - "@jest/types" "^25.1.0" - chalk "^3.0.0" - jest-get-type "^25.1.0" - jest-util "^25.1.0" - pretty-format "^25.1.0" - jest-each@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-each/-/jest-each-26.0.1.tgz#633083061619302fc90dd8f58350f9d77d67be04" @@ -12419,18 +11290,6 @@ jest-each@^26.0.1: jest-util "^26.0.1" pretty-format "^26.0.1" -jest-environment-jsdom@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.1.0.tgz#6777ab8b3e90fd076801efd3bff8e98694ab43c3" - integrity sha512-ILb4wdrwPAOHX6W82GGDUiaXSSOE274ciuov0lztOIymTChKFtC02ddyicRRCdZlB5YSrv3vzr1Z5xjpEe1OHQ== - dependencies: - "@jest/environment" "^25.1.0" - "@jest/fake-timers" "^25.1.0" - "@jest/types" "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" - jsdom "^15.1.1" - jest-environment-jsdom@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.0.1.tgz#217690852e5bdd7c846a4e3b50c8ffd441dfd249" @@ -12443,17 +11302,6 @@ jest-environment-jsdom@^26.0.1: jest-util "^26.0.1" jsdom "^16.2.2" -jest-environment-node@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.1.0.tgz#797bd89b378cf0bd794dc8e3dca6ef21126776db" - integrity sha512-U9kFWTtAPvhgYY5upnH9rq8qZkj6mYLup5l1caAjjx9uNnkLHN2xgZy5mo4SyLdmrh/EtB9UPpKFShvfQHD0Iw== - dependencies: - "@jest/environment" "^25.1.0" - "@jest/fake-timers" "^25.1.0" - "@jest/types" "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" - jest-environment-node@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.0.1.tgz#584a9ff623124ff6eeb49e0131b5f7612b310b13" @@ -12481,11 +11329,6 @@ jest-fetch-mock@^3.0.3: cross-fetch "^3.0.4" promise-polyfill "^8.1.3" -jest-get-type@^24.9.0: - version "24.9.0" - resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" - integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== - jest-get-type@^25.1.0: version "25.1.0" resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876" @@ -12501,24 +11344,6 @@ jest-get-type@^26.0.0: resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.0.0.tgz#381e986a718998dbfafcd5ec05934be538db4039" integrity sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg== -jest-haste-map@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.1.0.tgz#ae12163d284f19906260aa51fd405b5b2e5a4ad3" - integrity sha512-/2oYINIdnQZAqyWSn1GTku571aAfs8NxzSErGek65Iu5o8JYb+113bZysRMcC/pjE5v9w0Yz+ldbj9NxrFyPyw== - dependencies: - "@jest/types" "^25.1.0" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.3" - jest-serializer "^25.1.0" - jest-util "^25.1.0" - jest-worker "^25.1.0" - micromatch "^4.0.2" - sane "^4.0.3" - walker "^1.0.7" - optionalDependencies: - fsevents "^2.1.2" - jest-haste-map@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.0.1.tgz#40dcc03c43ac94d25b8618075804d09cd5d49de7" @@ -12539,29 +11364,6 @@ jest-haste-map@^26.0.1: optionalDependencies: fsevents "^2.1.2" -jest-jasmine2@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.1.0.tgz#681b59158a430f08d5d0c1cce4f01353e4b48137" - integrity sha512-GdncRq7jJ7sNIQ+dnXvpKO2MyP6j3naNK41DTTjEAhLEdpImaDA9zSAZwDhijjSF/D7cf4O5fdyUApGBZleaEg== - dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^25.1.0" - "@jest/source-map" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" - chalk "^3.0.0" - co "^4.6.0" - expect "^25.1.0" - is-generator-fn "^2.0.0" - jest-each "^25.1.0" - jest-matcher-utils "^25.1.0" - jest-message-util "^25.1.0" - jest-runtime "^25.1.0" - jest-snapshot "^25.1.0" - jest-util "^25.1.0" - pretty-format "^25.1.0" - throat "^5.0.0" - jest-jasmine2@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.0.1.tgz#947c40ee816636ba23112af3206d6fa7b23c1c1c" @@ -12585,25 +11387,6 @@ jest-jasmine2@^26.0.1: pretty-format "^26.0.1" throat "^5.0.0" -jest-junit@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/jest-junit/-/jest-junit-10.0.0.tgz#c94b91c24920a327c9d2a075e897b2dba4af494b" - integrity sha512-dbOVRyxHprdSpwSAR9/YshLwmnwf+RSl5hf0kCGlhAcEeZY9aRqo4oNmaT0tLC16Zy9D0zekDjWkjHGjXlglaQ== - dependencies: - jest-validate "^24.9.0" - mkdirp "^0.5.1" - strip-ansi "^5.2.0" - uuid "^3.3.3" - xml "^1.0.1" - -jest-leak-detector@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.1.0.tgz#ed6872d15aa1c72c0732d01bd073dacc7c38b5c6" - integrity sha512-3xRI264dnhGaMHRvkFyEKpDeaRzcEBhyNrOG5oT8xPxOyUAblIAQnpiR3QXu4wDor47MDTiHbiFcbypdLcLW5w== - dependencies: - jest-get-type "^25.1.0" - pretty-format "^25.1.0" - jest-leak-detector@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.0.1.tgz#79b19ab3f41170e0a78eb8fa754a116d3447fb8c" @@ -12632,20 +11415,6 @@ jest-matcher-utils@^26.0.1: jest-get-type "^26.0.0" pretty-format "^26.0.1" -jest-message-util@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.1.0.tgz#702a9a5cb05c144b9aa73f06e17faa219389845e" - integrity sha512-Nr/Iwar2COfN22aCqX0kCVbXgn8IBm9nWf4xwGr5Olv/KZh0CZ32RKgZWMVDXGdOahicM10/fgjdimGNX/ttCQ== - dependencies: - "@babel/code-frame" "^7.0.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" - "@types/stack-utils" "^1.0.1" - chalk "^3.0.0" - micromatch "^4.0.2" - slash "^3.0.0" - stack-utils "^1.0.1" - jest-message-util@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.0.1.tgz#07af1b42fc450b4cc8e90e4c9cef11b33ce9b0ac" @@ -12660,13 +11429,6 @@ jest-message-util@^26.0.1: slash "^3.0.0" stack-utils "^2.0.2" -jest-mock@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-25.1.0.tgz#411d549e1b326b7350b2e97303a64715c28615fd" - integrity sha512-28/u0sqS+42vIfcd1mlcg4ZVDmSUYuNvImP4X2lX5hRMLW+CN0BeiKVD4p+ujKKbSPKd3rg/zuhCF+QBLJ4vag== - dependencies: - "@jest/types" "^25.1.0" - jest-mock@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-26.0.1.tgz#7fd1517ed4955397cf1620a771dc2d61fad8fd40" @@ -12679,25 +11441,11 @@ jest-pnp-resolver@^1.2.1: resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== -jest-regex-util@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.1.0.tgz#efaf75914267741838e01de24da07b2192d16d87" - integrity sha512-9lShaDmDpqwg+xAd73zHydKrBbbrIi08Kk9YryBEBybQFg/lBWR/2BDjjiSE7KIppM9C5+c03XiDaZ+m4Pgs1w== - jest-regex-util@^26.0.0: version "26.0.0" resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-resolve-dependencies@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.1.0.tgz#8a1789ec64eb6aaa77fd579a1066a783437e70d2" - integrity sha512-Cu/Je38GSsccNy4I2vL12ZnBlD170x2Oh1devzuM9TLH5rrnLW1x51lN8kpZLYTvzx9j+77Y5pqBaTqfdzVzrw== - dependencies: - "@jest/types" "^25.1.0" - jest-regex-util "^25.1.0" - jest-snapshot "^25.1.0" - jest-resolve-dependencies@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.0.1.tgz#607ba7ccc32151d185a477cff45bf33bce417f0b" @@ -12707,17 +11455,6 @@ jest-resolve-dependencies@^26.0.1: jest-regex-util "^26.0.0" jest-snapshot "^26.0.1" -jest-resolve@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.1.0.tgz#23d8b6a4892362baf2662877c66aa241fa2eaea3" - integrity sha512-XkBQaU1SRCHj2Evz2Lu4Czs+uIgJXWypfO57L7JYccmAXv4slXA6hzNblmcRmf7P3cQ1mE7fL3ABV6jAwk4foQ== - dependencies: - "@jest/types" "^25.1.0" - browser-resolve "^1.11.3" - chalk "^3.0.0" - jest-pnp-resolver "^1.2.1" - realpath-native "^1.1.0" - jest-resolve@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.0.1.tgz#21d1ee06f9ea270a343a8893051aeed940cde736" @@ -12732,31 +11469,6 @@ jest-resolve@^26.0.1: resolve "^1.17.0" slash "^3.0.0" -jest-runner@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-25.1.0.tgz#fef433a4d42c89ab0a6b6b268e4a4fbe6b26e812" - integrity sha512-su3O5fy0ehwgt+e8Wy7A8CaxxAOCMzL4gUBftSs0Ip32S0epxyZPDov9Znvkl1nhVOJNf4UwAsnqfc3plfQH9w== - dependencies: - "@jest/console" "^25.1.0" - "@jest/environment" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" - chalk "^3.0.0" - exit "^0.1.2" - graceful-fs "^4.2.3" - jest-config "^25.1.0" - jest-docblock "^25.1.0" - jest-haste-map "^25.1.0" - jest-jasmine2 "^25.1.0" - jest-leak-detector "^25.1.0" - jest-message-util "^25.1.0" - jest-resolve "^25.1.0" - jest-runtime "^25.1.0" - jest-util "^25.1.0" - jest-worker "^25.1.0" - source-map-support "^0.5.6" - throat "^5.0.0" - jest-runner@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-26.0.1.tgz#ea03584b7ae4bacfb7e533d680a575a49ae35d50" @@ -12782,37 +11494,6 @@ jest-runner@^26.0.1: source-map-support "^0.5.6" throat "^5.0.0" -jest-runtime@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.1.0.tgz#02683218f2f95aad0f2ec1c9cdb28c1dc0ec0314" - integrity sha512-mpPYYEdbExKBIBB16ryF6FLZTc1Rbk9Nx0ryIpIMiDDkOeGa0jQOKVI/QeGvVGlunKKm62ywcioeFVzIbK03bA== - dependencies: - "@jest/console" "^25.1.0" - "@jest/environment" "^25.1.0" - "@jest/source-map" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" - "@types/yargs" "^15.0.0" - chalk "^3.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.3" - jest-config "^25.1.0" - jest-haste-map "^25.1.0" - jest-message-util "^25.1.0" - jest-mock "^25.1.0" - jest-regex-util "^25.1.0" - jest-resolve "^25.1.0" - jest-snapshot "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" - realpath-native "^1.1.0" - slash "^3.0.0" - strip-bom "^4.0.0" - yargs "^15.0.0" - jest-runtime@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.0.1.tgz#a121a6321235987d294168e282d52b364d7d3f89" @@ -12845,11 +11526,6 @@ jest-runtime@^26.0.1: strip-bom "^4.0.0" yargs "^15.3.1" -jest-serializer@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.1.0.tgz#73096ba90e07d19dec4a0c1dd89c355e2f129e5d" - integrity sha512-20Wkq5j7o84kssBwvyuJ7Xhn7hdPeTXndnwIblKDR2/sy1SUm6rWWiG9kSCgJPIfkDScJCIsTtOKdlzfIHOfKA== - jest-serializer@^26.0.0: version "26.0.0" resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.0.0.tgz#f6c521ddb976943b93e662c0d4d79245abec72a3" @@ -12857,25 +11533,6 @@ jest-serializer@^26.0.0: dependencies: graceful-fs "^4.2.4" -jest-snapshot@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.1.0.tgz#d5880bd4b31faea100454608e15f8d77b9d221d9" - integrity sha512-xZ73dFYN8b/+X2hKLXz4VpBZGIAn7muD/DAg+pXtDzDGw3iIV10jM7WiHqhCcpDZfGiKEj7/2HXAEPtHTj0P2A== - dependencies: - "@babel/types" "^7.0.0" - "@jest/types" "^25.1.0" - chalk "^3.0.0" - expect "^25.1.0" - jest-diff "^25.1.0" - jest-get-type "^25.1.0" - jest-matcher-utils "^25.1.0" - jest-message-util "^25.1.0" - jest-resolve "^25.1.0" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - pretty-format "^25.1.0" - semver "^7.1.1" - jest-snapshot@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.0.1.tgz#1baa942bd83d47b837a84af7fcf5fd4a236da399" @@ -12897,16 +11554,6 @@ jest-snapshot@^26.0.1: pretty-format "^26.0.1" semver "^7.3.2" -jest-util@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-util/-/jest-util-25.1.0.tgz#7bc56f7b2abd534910e9fa252692f50624c897d9" - integrity sha512-7did6pLQ++87Qsj26Fs/TIwZMUFBXQ+4XXSodRNy3luch2DnRXsSnmpVtxxQ0Yd6WTipGpbhh2IFP1mq6/fQGw== - dependencies: - "@jest/types" "^25.1.0" - chalk "^3.0.0" - is-ci "^2.0.0" - mkdirp "^0.5.1" - jest-util@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-util/-/jest-util-26.0.1.tgz#72c4c51177b695fdd795ca072a6f94e3d7cef00a" @@ -12918,30 +11565,6 @@ jest-util@^26.0.1: is-ci "^2.0.0" make-dir "^3.0.0" -jest-validate@^24.9.0: - version "24.9.0" - resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" - integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ== - dependencies: - "@jest/types" "^24.9.0" - camelcase "^5.3.1" - chalk "^2.0.1" - jest-get-type "^24.9.0" - leven "^3.1.0" - pretty-format "^24.9.0" - -jest-validate@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-25.1.0.tgz#1469fa19f627bb0a9a98e289f3e9ab6a668c732a" - integrity sha512-kGbZq1f02/zVO2+t1KQGSVoCTERc5XeObLwITqC6BTRH3Adv7NZdYqCpKIZLUgpLXf2yISzQ465qOZpul8abXA== - dependencies: - "@jest/types" "^25.1.0" - camelcase "^5.3.1" - chalk "^3.0.0" - jest-get-type "^25.1.0" - leven "^3.1.0" - pretty-format "^25.1.0" - jest-validate@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-26.0.1.tgz#a62987e1da5b7f724130f904725e22f4e5b2e23c" @@ -12954,18 +11577,6 @@ jest-validate@^26.0.1: leven "^3.1.0" pretty-format "^26.0.1" -jest-watcher@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.1.0.tgz#97cb4a937f676f64c9fad2d07b824c56808e9806" - integrity sha512-Q9eZ7pyaIr6xfU24OeTg4z1fUqBF/4MP6J801lyQfg7CsnZ/TCzAPvCfckKdL5dlBBEKBeHV0AdyjFZ5eWj4ig== - dependencies: - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" - ansi-escapes "^4.2.1" - chalk "^3.0.0" - jest-util "^25.1.0" - string-length "^3.1.0" - jest-watcher@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.0.1.tgz#5b5e3ebbdf10c240e22a98af66d645631afda770" @@ -12994,15 +11605,6 @@ jest-worker@^26.0.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest/-/jest-25.1.0.tgz#b85ef1ddba2fdb00d295deebbd13567106d35be9" - integrity sha512-FV6jEruneBhokkt9MQk0WUFoNTwnF76CLXtwNMfsc0um0TlB/LG2yxUd0KqaFjEJ9laQmVWQWS0sG/t2GsuI0w== - dependencies: - "@jest/core" "^25.1.0" - import-local "^3.0.2" - jest-cli "^25.1.0" - jest@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest/-/jest-26.0.1.tgz#5c51a2e58dff7525b65f169721767173bf832694" @@ -13072,38 +11674,6 @@ jsdom@11.12.0: ws "^5.2.0" xml-name-validator "^3.0.0" -jsdom@^15.1.1: - version "15.2.1" - resolved "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5" - integrity sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g== - dependencies: - abab "^2.0.0" - acorn "^7.1.0" - acorn-globals "^4.3.2" - array-equal "^1.0.0" - cssom "^0.4.1" - cssstyle "^2.0.0" - data-urls "^1.1.0" - domexception "^1.0.1" - escodegen "^1.11.1" - html-encoding-sniffer "^1.0.2" - nwsapi "^2.2.0" - parse5 "5.1.0" - pn "^1.1.0" - request "^2.88.0" - request-promise-native "^1.0.7" - saxes "^3.1.9" - symbol-tree "^3.2.2" - tough-cookie "^3.0.1" - w3c-hr-time "^1.0.1" - w3c-xmlserializer "^1.1.2" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.3.0" - whatwg-url "^7.0.0" - ws "^7.0.0" - xml-name-validator "^3.0.0" - jsdom@^16.2.2: version "16.2.2" resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.2.2.tgz#76f2f7541646beb46a938f5dc476b88705bedf2b" @@ -13393,13 +11963,6 @@ kuler@1.0.x: dependencies: colornames "^1.1.1" -latest-version@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" - integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU= - dependencies: - package-json "^4.0.0" - latest-version@^5.0.0: version "5.1.0" resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" @@ -13422,11 +11985,6 @@ lazy-cache@^1.0.3: resolved "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= -lazy-property@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/lazy-property/-/lazy-property-1.0.0.tgz#84ddc4b370679ba8bd4cdcfa4c06b43d57111147" - integrity sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc= - lazy-universal-dotenv@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/lazy-universal-dotenv/-/lazy-universal-dotenv-3.0.1.tgz#a6c8938414bca426ab8c9463940da451a911db38" @@ -13438,20 +11996,6 @@ lazy-universal-dotenv@^3.0.1: dotenv "^8.0.0" dotenv-expand "^5.1.0" -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - left-pad@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" @@ -13493,7 +12037,15 @@ levenary@^1.1.1: dependencies: leven "^3.1.0" -levn@^0.3.0, levn@~0.3.0: +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +levn@~0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= @@ -13501,140 +12053,6 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -libcipm@^4.0.7: - version "4.0.7" - resolved "https://registry.npmjs.org/libcipm/-/libcipm-4.0.7.tgz#76cd675c98bdaae64db88b782b01b804b6d02c8a" - integrity sha512-fTq33otU3PNXxxCTCYCYe7V96o59v/o7bvtspmbORXpgFk+wcWrGf5x6tBgui5gCed/45/wtPomBsZBYm5KbIw== - dependencies: - bin-links "^1.1.2" - bluebird "^3.5.1" - figgy-pudding "^3.5.1" - find-npm-prefix "^1.0.2" - graceful-fs "^4.1.11" - ini "^1.3.5" - lock-verify "^2.0.2" - mkdirp "^0.5.1" - npm-lifecycle "^3.0.0" - npm-logical-tree "^1.2.1" - npm-package-arg "^6.1.0" - pacote "^9.1.0" - read-package-json "^2.0.13" - rimraf "^2.6.2" - worker-farm "^1.6.0" - -libnpm@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/libnpm/-/libnpm-3.0.1.tgz#0be11b4c9dd4d1ffd7d95c786e92e55d65be77a2" - integrity sha512-d7jU5ZcMiTfBqTUJVZ3xid44fE5ERBm9vBnmhp2ECD2Ls+FNXWxHSkO7gtvrnbLO78gwPdNPz1HpsF3W4rjkBQ== - dependencies: - bin-links "^1.1.2" - bluebird "^3.5.3" - find-npm-prefix "^1.0.2" - libnpmaccess "^3.0.2" - libnpmconfig "^1.2.1" - libnpmhook "^5.0.3" - libnpmorg "^1.0.1" - libnpmpublish "^1.1.2" - libnpmsearch "^2.0.2" - libnpmteam "^1.0.2" - lock-verify "^2.0.2" - npm-lifecycle "^3.0.0" - npm-logical-tree "^1.2.1" - npm-package-arg "^6.1.0" - npm-profile "^4.0.2" - npm-registry-fetch "^4.0.0" - npmlog "^4.1.2" - pacote "^9.5.3" - read-package-json "^2.0.13" - stringify-package "^1.0.0" - -libnpmaccess@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-3.0.2.tgz#8b2d72345ba3bef90d3b4f694edd5c0417f58923" - integrity sha512-01512AK7MqByrI2mfC7h5j8N9V4I7MHJuk9buo8Gv+5QgThpOgpjB7sQBDDkeZqRteFb1QM/6YNdHfG7cDvfAQ== - dependencies: - aproba "^2.0.0" - get-stream "^4.0.0" - npm-package-arg "^6.1.0" - npm-registry-fetch "^4.0.0" - -libnpmconfig@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/libnpmconfig/-/libnpmconfig-1.2.1.tgz#c0c2f793a74e67d4825e5039e7a02a0044dfcbc0" - integrity sha512-9esX8rTQAHqarx6qeZqmGQKBNZR5OIbl/Ayr0qQDy3oXja2iFVQQI81R6GZ2a02bSNZ9p3YOGX1O6HHCb1X7kA== - dependencies: - figgy-pudding "^3.5.1" - find-up "^3.0.0" - ini "^1.3.5" - -libnpmhook@^5.0.3: - version "5.0.3" - resolved "https://registry.npmjs.org/libnpmhook/-/libnpmhook-5.0.3.tgz#4020c0f5edbf08ebe395325caa5ea01885b928f7" - integrity sha512-UdNLMuefVZra/wbnBXECZPefHMGsVDTq5zaM/LgKNE9Keyl5YXQTnGAzEo+nFOpdRqTWI9LYi4ApqF9uVCCtuA== - dependencies: - aproba "^2.0.0" - figgy-pudding "^3.4.1" - get-stream "^4.0.0" - npm-registry-fetch "^4.0.0" - -libnpmorg@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/libnpmorg/-/libnpmorg-1.0.1.tgz#5d2503f6ceb57f33dbdcc718e6698fea6d5ad087" - integrity sha512-0sRUXLh+PLBgZmARvthhYXQAWn0fOsa6T5l3JSe2n9vKG/lCVK4nuG7pDsa7uMq+uTt2epdPK+a2g6btcY11Ww== - dependencies: - aproba "^2.0.0" - figgy-pudding "^3.4.1" - get-stream "^4.0.0" - npm-registry-fetch "^4.0.0" - -libnpmpublish@^1.1.2: - version "1.1.3" - resolved "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-1.1.3.tgz#e3782796722d79eef1a0a22944c117e0c4ca4280" - integrity sha512-/3LsYqVc52cHXBmu26+J8Ed7sLs/hgGVFMH1mwYpL7Qaynb9RenpKqIKu0sJ130FB9PMkpMlWjlbtU8A4m7CQw== - dependencies: - aproba "^2.0.0" - figgy-pudding "^3.5.1" - get-stream "^4.0.0" - lodash.clonedeep "^4.5.0" - normalize-package-data "^2.4.0" - npm-package-arg "^6.1.0" - npm-registry-fetch "^4.0.0" - semver "^5.5.1" - ssri "^6.0.1" - -libnpmsearch@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/libnpmsearch/-/libnpmsearch-2.0.2.tgz#9a4f059102d38e3dd44085bdbfe5095f2a5044cf" - integrity sha512-VTBbV55Q6fRzTdzziYCr64+f8AopQ1YZ+BdPOv16UegIEaE8C0Kch01wo4s3kRTFV64P121WZJwgmBwrq68zYg== - dependencies: - figgy-pudding "^3.5.1" - get-stream "^4.0.0" - npm-registry-fetch "^4.0.0" - -libnpmteam@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/libnpmteam/-/libnpmteam-1.0.2.tgz#8b48bcbb6ce70dd8150c950fcbdbf3feb6eec820" - integrity sha512-p420vM28Us04NAcg1rzgGW63LMM6rwe+6rtZpfDxCcXxM0zUTLl7nPFEnRF3JfFBF5skF/yuZDUthTsHgde8QA== - dependencies: - aproba "^2.0.0" - figgy-pudding "^3.4.1" - get-stream "^4.0.0" - npm-registry-fetch "^4.0.0" - -libnpx@^10.2.2: - version "10.2.2" - resolved "https://registry.npmjs.org/libnpx/-/libnpx-10.2.2.tgz#5a4171b9b92dd031463ef66a4af9f5cbd6b09572" - integrity sha512-ujaYToga1SAX5r7FU5ShMFi88CWpY75meNZtr6RtEyv4l2ZK3+Wgvxq2IqlwWBiDZOqhumdeiocPS1aKrCMe3A== - dependencies: - dotenv "^5.0.1" - npm-package-arg "^6.0.0" - rimraf "^2.6.2" - safe-buffer "^5.1.0" - update-notifier "^2.3.0" - which "^1.3.0" - y18n "^4.0.0" - yargs "^11.0.0" - liftoff@3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" @@ -13661,25 +12079,6 @@ linkify-it@^2.0.0: dependencies: uc.micro "^1.0.1" -lint-staged@^10.0.4: - version "10.0.8" - resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.0.8.tgz#0f7849cdc336061f25f5d4fcbcfa385701ff4739" - integrity sha512-Oa9eS4DJqvQMVdywXfEor6F4vP+21fPHF8LUXgBbVWUSWBddjqsvO6Bv1LwMChmgQZZqwUvgJSHlu8HFHAPZmA== - dependencies: - chalk "^3.0.0" - commander "^4.0.1" - cosmiconfig "^6.0.0" - debug "^4.1.1" - dedent "^0.7.0" - execa "^3.4.0" - listr "^0.14.3" - log-symbols "^3.0.0" - micromatch "^4.0.2" - normalize-path "^3.0.0" - please-upgrade-node "^3.2.0" - string-argv "0.3.1" - stringify-object "^3.3.0" - lint-staged@^10.1.0: version "10.1.0" resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.1.0.tgz#18785bb005d5ed404f1c1db6563e082f7a7baac2" @@ -13840,61 +12239,22 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lock-verify@^2.0.2, lock-verify@^2.1.0: - version "2.2.0" - resolved "https://registry.npmjs.org/lock-verify/-/lock-verify-2.2.0.tgz#12432feb68bb647071c78c44bde16029a0f7d935" - integrity sha512-BhM1Vqsu7x0s+EalTifNjdDPks+ZjdAhComvnA6VcCIlDOI5ouELXqAe1BYuEIP4zGN0W08xVm6byJV1LnCiJg== - dependencies: - "@iarna/cli" "^1.2.0" - npm-package-arg "^6.1.0" - semver "^5.4.1" - -lockfile@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609" - integrity sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA== - dependencies: - signal-exit "^3.0.2" - lodash-es@^4.17.11: version "4.17.15" resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== -lodash._baseuniq@~4.6.0: - version "4.6.0" - resolved "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" - integrity sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg= - dependencies: - lodash._createset "~4.0.0" - lodash._root "~3.0.0" - -lodash._createset@~4.0.0: - version "4.0.3" - resolved "https://registry.npmjs.org/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" - integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= - lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= -lodash._root@~3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" - integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI= - lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= -lodash.capitalize@^4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" - integrity sha1-+CbJtOKoUR2E46yinbBeGk87cqk= - -lodash.clonedeep@^4.5.0, lodash.clonedeep@~4.5.0: +lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= @@ -13904,11 +12264,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= -lodash.escaperegexp@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" - integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= - lodash.flattendeep@^4.0.0: version "4.4.0" resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -13924,21 +12279,6 @@ lodash.ismatch@^4.4.0: resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - -lodash.map@^4.5.1: - version "4.6.0" - resolved "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" - integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM= - lodash.memoize@4.x, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -13979,31 +12319,11 @@ lodash.throttle@^4.1.1: resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= -lodash.toarray@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" - integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= - -lodash.union@~4.6.0: - version "4.6.0" - resolved "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" - integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= - -lodash.uniq@^4.5.0, lodash.uniq@~4.5.0: +lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash.uniqby@^4.7.0: - version "4.7.0" - resolved "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" - integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= - -lodash.without@~4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" - integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw= - lodash@4.17.15, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1: version "4.17.15" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -14048,18 +12368,6 @@ loglevel@^1.6.8: resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== -lolex@^5.0.0: - version "5.1.2" - resolved "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367" - integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A== - dependencies: - "@sinonjs/commons" "^1.7.0" - -longest@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8" - integrity sha1-eB4YMpaqlPbU2RbcM10NF676I/g= - loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -14193,13 +12501,6 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - map-cache@^0.2.0, map-cache@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -14256,23 +12557,6 @@ markdown-to-jsx@^6.9.1, markdown-to-jsx@^6.9.3: prop-types "^15.6.2" unquote "^1.1.0" -marked-terminal@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/marked-terminal/-/marked-terminal-4.0.0.tgz#2c7aa2c0eec496f05cd61f768d80d35db0bf6a86" - integrity sha512-mzU3VD7aVz12FfGoKFAceijehA6Ocjfg3rVimvJbFAB/NOYCsuzRVtq3PSFdPmWI5mhdGeEh3/aMJ5DSxAz94Q== - dependencies: - ansi-escapes "^4.3.0" - cardinal "^2.1.1" - chalk "^3.0.0" - cli-table "^0.3.1" - node-emoji "^1.10.0" - supports-hyperlinks "^2.0.0" - -marked@^0.8.0: - version "0.8.1" - resolved "https://registry.npmjs.org/marked/-/marked-0.8.1.tgz#a233f39572fab15ede53a3c3be8a139bff86d2dd" - integrity sha512-tZfJS8uE0zpo7xpTffwFwYRfW9AzNcdo04Qcjs+C9+oCy8MSRD2reD5iDVtYx8mtLaqsGughw/YLlcwNxAHA1g== - material-table@^1.58.0: version "1.58.2" resolved "https://registry.npmjs.org/material-table/-/material-table-1.58.2.tgz#dc0d19652848e6bb92f747d122bd7d4681cca6dc" @@ -14320,32 +12604,11 @@ mdurl@^1.0.1: resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= -meant@~1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/meant/-/meant-1.0.1.tgz#66044fea2f23230ec806fb515efea29c44d2115d" - integrity sha512-UakVLFjKkbbUwNWJ2frVLnnAtbb7D7DsloxRd3s/gDpI8rdv8W5Hp3NaDb+POBI1fQdeussER6NB8vpcRURvlg== - media-typer@0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -mem@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" - integrity sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y= - dependencies: - mimic-fn "^1.0.0" - -mem@^4.0.0: - version "4.3.0" - resolved "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - memoize-one@^5.0.4: version "5.1.1" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" @@ -14374,21 +12637,6 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" -meow@5.0.0, meow@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" - integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== - dependencies: - camelcase-keys "^4.0.0" - decamelize-keys "^1.0.0" - loud-rejection "^1.0.0" - minimist-options "^3.0.1" - normalize-package-data "^2.3.4" - read-pkg-up "^3.0.0" - redent "^2.0.0" - trim-newlines "^2.0.0" - yargs-parser "^10.0.0" - meow@^3.3.0: version "3.7.0" resolved "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -14420,6 +12668,21 @@ meow@^4.0.0: redent "^2.0.0" trim-newlines "^2.0.0" +meow@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" + integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== + dependencies: + camelcase-keys "^4.0.0" + decamelize-keys "^1.0.0" + loud-rejection "^1.0.0" + minimist-options "^3.0.1" + normalize-package-data "^2.3.4" + read-pkg-up "^3.0.0" + redent "^2.0.0" + trim-newlines "^2.0.0" + yargs-parser "^10.0.0" + merge-deep@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2" @@ -14444,11 +12707,6 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== -merge@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== - methods@^1.0.0, methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -14528,7 +12786,7 @@ mime@1.6.0, mime@^1.4.1: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.3.1, mime@^2.4.3, mime@^2.4.4: +mime@^2.3.1, mime@^2.4.4: version "2.4.4" resolved "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== @@ -14538,7 +12796,7 @@ mimic-fn@^1.0.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mimic-fn@^2.0.0, mimic-fn@^2.1.0: +mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -14613,11 +12871,6 @@ minimist-options@^3.0.1: arrify "^1.0.1" is-plain-obj "^1.1.0" -minimist@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - minimist@1.2.5, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -14728,7 +12981,7 @@ mkdirp@*, mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -14904,11 +13157,6 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== -nerf-dart@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz#e6dab7febf5ad816ea81cf5c629c5a0ebde72c1a" - integrity sha1-5tq3/r9a2Bbqgc9cYpxaDr3nLBo= - nice-try@^1.0.4: version "1.0.5" resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -14939,13 +13187,6 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" -node-emoji@^1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" - integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw== - dependencies: - lodash.toarray "^4.4.0" - node-fetch-npm@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.3.tgz#efae4aacb0500444e449a51fc1467397775ebc38" @@ -14965,7 +13206,7 @@ node-forge@0.9.0: resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== -node-gyp@^5.0.2, node-gyp@^5.1.0: +node-gyp@^5.0.2: version "5.1.0" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz#8e31260a7af4a2e2f994b0673d4e0b3866156332" integrity sha512-OUTryc5bt/P8zVgNUmC6xdXiDJxLMAW8cF5tLQOT9E5sOQj+UeQxnnPy74K3CLCa/SOjjBlbuzDLR8ANwA+wmw== @@ -15021,17 +13262,6 @@ node-modules-regexp@^1.0.0: resolved "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= -node-notifier@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz#cea319e06baa16deec8ce5cd7f133c4a46b68e12" - integrity sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw== - dependencies: - growly "^1.3.0" - is-wsl "^2.1.1" - semver "^6.3.0" - shellwords "^0.1.1" - which "^1.3.1" - node-notifier@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/node-notifier/-/node-notifier-7.0.0.tgz#513bc42f2aa3a49fce1980a7ff375957c71f718a" @@ -15083,7 +13313,7 @@ nodemon@^2.0.2: undefsafe "^2.0.2" update-notifier "^4.0.0" -nopt@^4.0.1, nopt@~4.0.1: +nopt@^4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== @@ -15145,19 +13375,6 @@ normalize-url@^4.1.0: resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== -normalize-url@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-5.0.0.tgz#f46c9dc20670495e4e18fbd1b4396e41d199f63c" - integrity sha512-bAEm2fx8Dq/a35Z6PIRkkBBJvR56BbEJvhpNtvCZ4W9FyORSna77fn+xtYFjqk5JpBS+fMnAOG/wFgkQBmB7hw== - -npm-audit-report@^1.3.2: - version "1.3.2" - resolved "https://registry.npmjs.org/npm-audit-report/-/npm-audit-report-1.3.2.tgz#303bc78cd9e4c226415076a4f7e528c89fc77018" - integrity sha512-abeqS5ONyXNaZJPGAf6TOUMNdSe1Y6cpc9MLBRn+CuUoYbfdca6AxOyXVlfIv9OgKX+cacblbG5w7A6ccwoTPw== - dependencies: - cli-table3 "^0.5.0" - console-control-strings "^1.1.0" - npm-bundled@^1.0.1: version "1.1.1" resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" @@ -15165,19 +13382,7 @@ npm-bundled@^1.0.1: dependencies: npm-normalize-package-bin "^1.0.1" -npm-cache-filename@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/npm-cache-filename/-/npm-cache-filename-1.0.2.tgz#ded306c5b0bfc870a9e9faf823bc5f283e05ae11" - integrity sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE= - -npm-install-checks@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-3.0.2.tgz#ab2e32ad27baa46720706908e5b14c1852de44d9" - integrity sha512-E4kzkyZDIWoin6uT5howP8VDvkM+E8IQDcHAycaAxMbwkqhIg5eEYALnXOl3Hq9MrkdQB/2/g1xwBINXdKSRkg== - dependencies: - semver "^2.3.0 || 3.x || 4 || 5" - -npm-lifecycle@^3.0.0, npm-lifecycle@^3.1.2, npm-lifecycle@^3.1.4: +npm-lifecycle@^3.1.2: version "3.1.4" resolved "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.4.tgz#de6975c7d8df65f5150db110b57cce498b0b604c" integrity sha512-tgs1PaucZwkxECGKhC/stbEgFyc3TGh2TJcg2CDr6jbvQRdteHNhmMeljRzpe4wgFAXQADoy1cSqqi7mtiAa5A== @@ -15191,17 +13396,12 @@ npm-lifecycle@^3.0.0, npm-lifecycle@^3.1.2, npm-lifecycle@^3.1.4: umask "^1.1.0" which "^1.3.1" -npm-logical-tree@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/npm-logical-tree/-/npm-logical-tree-1.2.1.tgz#44610141ca24664cad35d1e607176193fd8f5b88" - integrity sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg== - npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== -"npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0, npm-package-arg@^6.1.1: +"npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: version "6.1.1" resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7" integrity sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg== @@ -15211,7 +13411,7 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: semver "^5.6.0" validate-npm-package-name "^3.0.0" -npm-packlist@^1.1.12, npm-packlist@^1.1.6, npm-packlist@^1.4.4, npm-packlist@^1.4.8: +npm-packlist@^1.1.6, npm-packlist@^1.4.4: version "1.4.8" resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== @@ -15220,7 +13420,7 @@ npm-packlist@^1.1.12, npm-packlist@^1.1.6, npm-packlist@^1.4.4, npm-packlist@^1. npm-bundled "^1.0.1" npm-normalize-package-bin "^1.0.1" -npm-pick-manifest@^3.0.0, npm-pick-manifest@^3.0.2: +npm-pick-manifest@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz#f4d9e5fd4be2153e5f4e5f9b7be8dc419a99abb7" integrity sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw== @@ -15229,28 +13429,6 @@ npm-pick-manifest@^3.0.0, npm-pick-manifest@^3.0.2: npm-package-arg "^6.0.0" semver "^5.4.1" -npm-profile@^4.0.2, npm-profile@^4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/npm-profile/-/npm-profile-4.0.4.tgz#28ee94390e936df6d084263ee2061336a6a1581b" - integrity sha512-Ta8xq8TLMpqssF0H60BXS1A90iMoM6GeKwsmravJ6wYjWwSzcYBTdyWa3DZCYqPutacBMEm7cxiOkiIeCUAHDQ== - dependencies: - aproba "^1.1.2 || 2" - figgy-pudding "^3.4.1" - npm-registry-fetch "^4.0.0" - -npm-registry-fetch@^4.0.0, npm-registry-fetch@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.3.tgz#3c2179e39e04f9348b1c2979545951d36bee8766" - integrity sha512-WGvUx0lkKFhu9MbiGFuT9nG2NpfQ+4dCJwRwwtK2HK5izJEvwDxMeUyqbuMS7N/OkpVCqDorV6rO5E4V9F8lJw== - dependencies: - JSONStream "^1.3.4" - bluebird "^3.5.1" - figgy-pudding "^3.4.1" - lru-cache "^5.1.1" - make-fetch-happen "^5.0.0" - npm-package-arg "^6.1.0" - safe-buffer "^5.2.0" - npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -15265,133 +13443,7 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" -npm-user-validate@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/npm-user-validate/-/npm-user-validate-1.0.0.tgz#8ceca0f5cea04d4e93519ef72d0557a75122e951" - integrity sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE= - -npm@^6.10.3: - version "6.14.3" - resolved "https://registry.npmjs.org/npm/-/npm-6.14.3.tgz#a122618543c6670765cf5e827cd996b5552f9b65" - integrity sha512-3tQYVEEdSGQGYoXhZvNqW8faqCidfMMaL387RdDo4Uu5kQy4IgvJ13NIsWVMQ6e3QWlbicNMSpFiyzYfMUuPDw== - dependencies: - JSONStream "^1.3.5" - abbrev "~1.1.1" - ansicolors "~0.3.2" - ansistyles "~0.1.3" - aproba "^2.0.0" - archy "~1.0.0" - bin-links "^1.1.7" - bluebird "^3.5.5" - byte-size "^5.0.1" - cacache "^12.0.3" - call-limit "^1.1.1" - chownr "^1.1.4" - ci-info "^2.0.0" - cli-columns "^3.1.2" - cli-table3 "^0.5.1" - cmd-shim "^3.0.3" - columnify "~1.5.4" - config-chain "^1.1.12" - detect-indent "~5.0.0" - detect-newline "^2.1.0" - dezalgo "~1.0.3" - editor "~1.0.0" - figgy-pudding "^3.5.1" - find-npm-prefix "^1.0.2" - fs-vacuum "~1.2.10" - fs-write-stream-atomic "~1.0.10" - gentle-fs "^2.3.0" - glob "^7.1.6" - graceful-fs "^4.2.3" - has-unicode "~2.0.1" - hosted-git-info "^2.8.8" - iferr "^1.0.2" - infer-owner "^1.0.4" - inflight "~1.0.6" - inherits "^2.0.4" - ini "^1.3.5" - init-package-json "^1.10.3" - is-cidr "^3.0.0" - json-parse-better-errors "^1.0.2" - lazy-property "~1.0.0" - libcipm "^4.0.7" - libnpm "^3.0.1" - libnpmaccess "^3.0.2" - libnpmhook "^5.0.3" - libnpmorg "^1.0.1" - libnpmsearch "^2.0.2" - libnpmteam "^1.0.2" - libnpx "^10.2.2" - lock-verify "^2.1.0" - lockfile "^1.0.4" - lodash._baseuniq "~4.6.0" - lodash.clonedeep "~4.5.0" - lodash.union "~4.6.0" - lodash.uniq "~4.5.0" - lodash.without "~4.4.0" - lru-cache "^5.1.1" - meant "~1.0.1" - mississippi "^3.0.0" - mkdirp "^0.5.3" - move-concurrently "^1.0.1" - node-gyp "^5.1.0" - nopt "~4.0.1" - normalize-package-data "^2.5.0" - npm-audit-report "^1.3.2" - npm-cache-filename "~1.0.2" - npm-install-checks "^3.0.2" - npm-lifecycle "^3.1.4" - npm-package-arg "^6.1.1" - npm-packlist "^1.4.8" - npm-pick-manifest "^3.0.2" - npm-profile "^4.0.4" - npm-registry-fetch "^4.0.3" - npm-user-validate "~1.0.0" - npmlog "~4.1.2" - once "~1.4.0" - opener "^1.5.1" - osenv "^0.1.5" - pacote "^9.5.12" - path-is-inside "~1.0.2" - promise-inflight "~1.0.1" - qrcode-terminal "^0.12.0" - query-string "^6.8.2" - qw "~1.0.1" - read "~1.0.7" - read-cmd-shim "^1.0.5" - read-installed "~4.0.3" - read-package-json "^2.1.1" - read-package-tree "^5.3.1" - readable-stream "^3.6.0" - readdir-scoped-modules "^1.1.0" - request "^2.88.0" - retry "^0.12.0" - rimraf "^2.7.1" - safe-buffer "^5.1.2" - semver "^5.7.1" - sha "^3.0.0" - slide "~1.1.6" - sorted-object "~2.0.1" - sorted-union-stream "~2.1.3" - ssri "^6.0.1" - stringify-package "^1.0.1" - tar "^4.4.13" - text-table "~0.2.0" - tiny-relative-date "^1.3.0" - uid-number "0.0.6" - umask "~1.1.0" - unique-filename "^1.1.1" - unpipe "~1.0.0" - update-notifier "^2.5.0" - uuid "^3.3.3" - validate-npm-package-license "^3.0.4" - validate-npm-package-name "~3.0.0" - which "^1.3.1" - worker-farm "^1.7.0" - write-file-atomic "^2.4.3" - -npmlog@^4.0.2, npmlog@^4.1.2, npmlog@~4.1.2: +npmlog@^4.0.2, npmlog@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -15564,7 +13616,7 @@ on-headers@~1.0.2: resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0, once@^1.3.1, once@^1.4.0, once@~1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -15615,11 +13667,6 @@ opencollective-postinstall@^2.0.2: resolved "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== -opener@^1.5.1: - version "1.5.1" - resolved "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed" - integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA== - opn@^5.5.0: version "5.5.0" resolved "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" @@ -15627,7 +13674,7 @@ opn@^5.5.0: dependencies: is-wsl "^1.1.0" -optionator@^0.8.1, optionator@^0.8.3: +optionator@^0.8.1: version "0.8.3" resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== @@ -15639,6 +13686,18 @@ optionator@^0.8.1, optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + ora@*, ora@^4.0.3: version "4.0.4" resolved "https://registry.npmjs.org/ora/-/ora-4.0.4.tgz#e8da697cc5b6a47266655bf68e0fb588d29a545d" @@ -15670,24 +13729,6 @@ os-homedir@^1.0.0: resolved "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" - integrity sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA== - dependencies: - execa "^0.7.0" - lcid "^1.0.0" - mem "^1.1.0" - -os-locale@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - os-name@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" @@ -15719,23 +13760,11 @@ p-cancelable@^1.0.0: resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - p-each-series@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ== -p-filter@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz#1b1472562ae7a0f742f0f3d3d3718ea66ff9c09c" - integrity sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw== - dependencies: - p-map "^2.0.0" - p-finally@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -15746,16 +13775,6 @@ p-finally@^2.0.0: resolved "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - -p-is-promise@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz#58e78c7dfe2e163cf2a04ff869e7c1dba64a5971" - integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== - p-limit@2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -15842,11 +13861,6 @@ p-reduce@^1.0.0: resolved "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= -p-reduce@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a" - integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== - p-retry@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" @@ -15854,14 +13868,6 @@ p-retry@^3.0.1: dependencies: retry "^0.12.0" -p-retry@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz#ea9066c6b44f23cab4cd42f6147cdbbc6604da5d" - integrity sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA== - dependencies: - "@types/retry" "^0.12.0" - retry "^0.12.0" - p-timeout@^3.1.0: version "3.2.0" resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -15886,16 +13892,6 @@ p-waterfall@^1.0.0: dependencies: p-reduce "^1.0.0" -package-json@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" - integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0= - dependencies: - got "^6.7.1" - registry-auth-token "^3.0.1" - registry-url "^3.0.3" - semver "^5.1.0" - package-json@^6.3.0: version "6.5.0" resolved "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" @@ -15906,42 +13902,6 @@ package-json@^6.3.0: registry-url "^5.0.0" semver "^6.2.0" -pacote@^9.1.0, pacote@^9.5.12, pacote@^9.5.3: - version "9.5.12" - resolved "https://registry.npmjs.org/pacote/-/pacote-9.5.12.tgz#1e11dd7a8d736bcc36b375a9804d41bb0377bf66" - integrity sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ== - dependencies: - bluebird "^3.5.3" - cacache "^12.0.2" - chownr "^1.1.2" - figgy-pudding "^3.5.1" - get-stream "^4.1.0" - glob "^7.1.3" - infer-owner "^1.0.4" - lru-cache "^5.1.1" - make-fetch-happen "^5.0.0" - minimatch "^3.0.4" - minipass "^2.3.5" - mississippi "^3.0.0" - mkdirp "^0.5.1" - normalize-package-data "^2.4.0" - npm-normalize-package-bin "^1.0.0" - npm-package-arg "^6.1.0" - npm-packlist "^1.1.12" - npm-pick-manifest "^3.0.0" - npm-registry-fetch "^4.0.0" - osenv "^0.1.5" - promise-inflight "^1.0.1" - promise-retry "^1.1.1" - protoduck "^5.0.1" - rimraf "^2.6.2" - safe-buffer "^5.1.2" - semver "^5.6.0" - ssri "^6.0.1" - tar "^4.4.10" - unique-filename "^1.1.1" - which "^1.3.1" - pako@~1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -16067,11 +14027,6 @@ parse5@4.0.0: resolved "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== -parse5@5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" - integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== - parse5@5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" @@ -16158,7 +14113,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.2: +path-is-inside@^1.0.1, path-is-inside@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= @@ -16312,14 +14267,6 @@ pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" -pkg-conf@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz#2126514ca6f2abfebd168596df18ba57867f0058" - integrity sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg= - dependencies: - find-up "^2.0.0" - load-json-file "^4.0.0" - pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" @@ -16807,12 +14754,17 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.4.0" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prepend-http@^1.0.0, prepend-http@^1.0.1: +prepend-http@^1.0.0: version "1.0.4" resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= @@ -16822,7 +14774,7 @@ prepend-http@^2.0.0: resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@^1.16.4, prettier@^1.18.2: +prettier@^1.16.4: version "1.19.1" resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== @@ -16845,7 +14797,7 @@ pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" -pretty-format@^24.3.0, pretty-format@^24.9.0: +pretty-format@^24.3.0: version "24.9.0" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== @@ -16914,7 +14866,7 @@ progress@^2.0.0: resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -promise-inflight@^1.0.1, promise-inflight@~1.0.1: +promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= @@ -17109,11 +15061,6 @@ q@^1.1.2, q@^1.5.1: resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qrcode-terminal@^0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" - integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== - qs@6.7.0: version "6.7.0" resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -17137,15 +15084,6 @@ query-string@^4.1.0: object-assign "^4.1.0" strict-uri-encode "^1.0.0" -query-string@^6.8.2: - version "6.11.1" - resolved "https://registry.npmjs.org/query-string/-/query-string-6.11.1.tgz#ab021f275d463ce1b61e88f0ce6988b3e8fe7c2c" - integrity sha512-1ZvJOUl8ifkkBxu2ByVM/8GijMIPx+cef7u3yroO3Ogm4DOdZcF5dcrWTIlSHe3Pg/mtlt6/eFjObDfJureZZA== - dependencies: - decode-uri-component "^0.2.0" - split-on-first "^1.0.0" - strict-uri-encode "^2.0.0" - querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -17166,11 +15104,6 @@ quick-lru@^1.0.0: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= -qw@~1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" - integrity sha1-77/cdA+a0FQwRCassYNBLMi5ltQ= - raf-schd@^4.0.0: version "4.0.2" resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" @@ -17239,7 +15172,7 @@ rc-progress@^3.0.0: dependencies: classnames "^2.2.6" -rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: +rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -17655,28 +15588,14 @@ react@^16.0.0, react@^16.12.0, react@^16.13.1, react@^16.8.3: object-assign "^4.1.1" prop-types "^15.6.2" -read-cmd-shim@^1.0.1, read-cmd-shim@^1.0.5: +read-cmd-shim@^1.0.1: version "1.0.5" resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" integrity sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA== dependencies: graceful-fs "^4.1.2" -read-installed@~4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067" - integrity sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc= - dependencies: - debuglog "^1.0.1" - read-package-json "^2.0.0" - readdir-scoped-modules "^1.0.0" - semver "2 || 3 || 4 || 5" - slide "~1.1.3" - util-extend "^1.0.1" - optionalDependencies: - graceful-fs "^4.1.2" - -"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.13, read-package-json@^2.1.1: +"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.13: version "2.1.1" resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.1.tgz#16aa66c59e7d4dad6288f179dd9295fd59bb98f1" integrity sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A== @@ -17688,7 +15607,7 @@ read-installed@~4.0.3: optionalDependencies: graceful-fs "^4.1.2" -read-package-tree@^5.1.6, read-package-tree@^5.3.1: +read-package-tree@^5.1.6: version "5.3.1" resolved "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== @@ -17721,7 +15640,7 @@ read-pkg-up@^3.0.0: find-up "^2.0.0" read-pkg "^3.0.0" -read-pkg-up@^7.0.0, read-pkg-up@^7.0.1: +read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== @@ -17757,7 +15676,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -read-pkg@^5.0.0, read-pkg@^5.2.0: +read-pkg@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== @@ -17767,7 +15686,7 @@ read-pkg@^5.0.0, read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -read@1, read@~1.0.1, read@~1.0.7: +read@1, read@~1.0.1: version "1.0.7" resolved "https://registry.npmjs.org/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= @@ -17787,7 +15706,7 @@ read@1, read@~1.0.1, read@~1.0.7: string_decoder "~1.1.1" util-deprecate "~1.0.1" -"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0: version "3.6.0" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -17796,17 +15715,7 @@ read@1, read@~1.0.1, read@~1.0.7: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@~1.1.10: - version "1.1.14" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readdir-scoped-modules@^1.0.0, readdir-scoped-modules@^1.1.0: +readdir-scoped-modules@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== @@ -17832,13 +15741,6 @@ readdirp@~3.4.0: dependencies: picomatch "^2.2.1" -realpath-native@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" - integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== - dependencies: - util.promisify "^1.0.0" - recast@^0.14.7: version "0.14.7" resolved "https://registry.npmjs.org/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" @@ -17887,13 +15789,6 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redeyed@~2.1.0: - version "2.1.1" - resolved "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b" - integrity sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs= - dependencies: - esprima "~4.0.0" - redux@^4.0.1: version "4.0.5" resolved "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" @@ -17928,11 +15823,6 @@ regenerate@^1.4.0: resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== -regenerator-runtime@^0.10.5: - version "0.10.5" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" - integrity sha1-M2w+/BIgrc7dosn6tntaeVWjNlg= - regenerator-runtime@^0.11.0: version "0.11.1" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" @@ -17967,16 +15857,16 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0: define-properties "^1.1.3" es-abstract "^1.17.0-next.1" -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== - regexpp@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz#dd63982ee3300e67b41c1956f850aa680d9d330e" integrity sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g== +regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + regexpu-core@^4.6.0, regexpu-core@^4.7.0: version "4.7.0" resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" @@ -17989,14 +15879,6 @@ regexpu-core@^4.6.0, regexpu-core@^4.7.0: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.2.0" -registry-auth-token@^3.0.1: - version "3.4.0" - resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e" - integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A== - dependencies: - rc "^1.1.6" - safe-buffer "^5.0.1" - registry-auth-token@^4.0.0: version "4.1.1" resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479" @@ -18004,13 +15886,6 @@ registry-auth-token@^4.0.0: dependencies: rc "^1.2.8" -registry-url@^3.0.3: - version "3.1.0" - resolved "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" - integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI= - dependencies: - rc "^1.0.1" - registry-url@^5.0.0: version "5.1.0" resolved "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" @@ -18117,7 +15992,7 @@ request-promise-core@1.1.3: dependencies: lodash "^4.17.15" -request-promise-native@^1.0.5, request-promise-native@^1.0.7, request-promise-native@^1.0.8: +request-promise-native@^1.0.5, request-promise-native@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== @@ -18157,11 +16032,6 @@ require-directory@^2.1.1: resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -18214,13 +16084,6 @@ resolve-from@^4.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-global@1.0.0, resolve-global@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz#a2a79df4af2ca3f49bf77ef9ddacd322dad19255" - integrity sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw== - dependencies: - global-dirs "^0.1.1" - resolve-pathname@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" @@ -18231,11 +16094,6 @@ resolve-url@^0.2.1: resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@1.1.7: - version "1.1.7" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= - resolve@1.15.1: version "1.15.1" resolved "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" @@ -18243,7 +16101,7 @@ resolve@1.15.1: dependencies: path-parse "^1.0.6" -resolve@1.x, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.15.1, resolve@^1.16.1, resolve@^1.17.0, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.15.1, resolve@^1.16.1, resolve@^1.17.0, resolve@^1.3.2: version "1.17.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -18318,11 +16176,6 @@ rifm@^0.7.0: dependencies: "@babel/runtime" "^7.3.1" -right-pad@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0" - integrity sha1-jKCMLLtbVedNr6lr9/0aJ9VoyNA= - rimraf@2.6.3: version "2.6.3" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -18330,7 +16183,7 @@ rimraf@2.6.3: dependencies: glob "^7.1.3" -rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1: +rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -18546,13 +16399,6 @@ sax@^1.2.4, sax@~1.2.4: resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -saxes@^3.1.9: - version "3.1.11" - resolved "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b" - integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g== - dependencies: - xmlchars "^2.1.1" - saxes@^5.0.0: version "5.0.1" resolved "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" @@ -18607,52 +16453,11 @@ selfsigned@^1.10.7: dependencies: node-forge "0.9.0" -semantic-release@^17.0.1: - version "17.0.4" - resolved "https://registry.npmjs.org/semantic-release/-/semantic-release-17.0.4.tgz#4ca739b2bf80f8ce5e49b05f12c15f49ca233d6d" - integrity sha512-5y9QRSrZtdvACmlpX5DvEVsvFuKRDUVn7JVJFxPVLGrGofDf1d0M/+hA1wFmCjiJZ+VCY8bYaSqVqF14KCF9rw== - dependencies: - "@semantic-release/commit-analyzer" "^8.0.0" - "@semantic-release/error" "^2.2.0" - "@semantic-release/github" "^7.0.0" - "@semantic-release/npm" "^7.0.0" - "@semantic-release/release-notes-generator" "^9.0.0" - aggregate-error "^3.0.0" - cosmiconfig "^6.0.0" - debug "^4.0.0" - env-ci "^5.0.0" - execa "^4.0.0" - figures "^3.0.0" - find-versions "^3.0.0" - get-stream "^5.0.0" - git-log-parser "^1.2.0" - hook-std "^2.0.0" - hosted-git-info "^3.0.0" - lodash "^4.17.15" - marked "^0.8.0" - marked-terminal "^4.0.0" - micromatch "^4.0.2" - p-each-series "^2.1.0" - p-reduce "^2.0.0" - read-pkg-up "^7.0.0" - resolve-from "^5.0.0" - semver "^7.1.1" - semver-diff "^3.1.1" - signale "^1.2.1" - yargs "^15.0.1" - semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= -semver-diff@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" - integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY= - dependencies: - semver "^5.0.3" - semver-diff@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" @@ -18665,26 +16470,26 @@ semver-regex@^2.0.0: resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.3.0, semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - semver@7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@7.x, semver@^7.1.1, semver@^7.1.2, semver@^7.2.1, semver@^7.3.2: +semver@7.x, semver@^7.2.1, semver@^7.3.2: version "7.3.2" resolved "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + send@0.17.1: version "0.17.1" resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -18786,13 +16591,6 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -sha@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/sha/-/sha-3.0.0.tgz#b2f2f90af690c16a3a839a6a6c680ea51fedd1ae" - integrity sha512-DOYnM37cNsLNSGIG/zZWch5CKIRNoLdYUQTQlcgkRkoYIUwDYjqDyye16YcDZg/OPdcbUgTKMjc4SY6TB7ZAPw== - dependencies: - graceful-fs "^4.1.2" - shallow-clone@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" @@ -18849,15 +16647,6 @@ shell-quote@1.7.2: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -shelljs@0.7.6: - version "0.7.6" - resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad" - integrity sha1-N5zM+1a5HIYB5HkzVutTgpJN6a0= - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - shelljs@^0.8.3: version "0.8.3" resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" @@ -18885,15 +16674,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -signale@^1.2.1: - version "1.4.0" - resolved "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz#c4be58302fb0262ac00fc3d886a7c113759042f1" - integrity sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w== - dependencies: - chalk "^2.3.2" - figures "^2.0.0" - pkg-conf "^2.1.0" - simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -18955,7 +16735,7 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" -slide@^1.1.6, slide@~1.1.3, slide@~1.1.6: +slide@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= @@ -19046,19 +16826,6 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" -sorted-object@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/sorted-object/-/sorted-object-2.0.1.tgz#7d631f4bd3a798a24af1dffcfbfe83337a5df5fc" - integrity sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw= - -sorted-union-stream@~2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/sorted-union-stream/-/sorted-union-stream-2.1.3.tgz#c7794c7e077880052ff71a8d4a2dbb4a9a638ac7" - integrity sha1-x3lMfgd4gAUv9xqNSi27Sppjisc= - dependencies: - from2 "^1.3.0" - stream-iterate "^1.1.0" - source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -19118,11 +16885,6 @@ space-separated-tokens@^1.0.0: resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== -spawn-error-forwarder@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz#1afd94738e999b0346d7b9fc373be55e07577029" - integrity sha1-Gv2Uc46ZmwNG17n8NzvlXgdXcCk= - spdx-correct@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" @@ -19177,11 +16939,6 @@ split-ca@^1.0.1: resolved "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" integrity sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY= -split-on-first@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" - integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== - split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -19196,13 +16953,6 @@ split2@^2.0.0: dependencies: through2 "^2.0.2" -split2@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/split2/-/split2-1.0.0.tgz#52e2e221d88c75f9a73f90556e263ff96772b314" - integrity sha1-UuLiIdiMdfmnP5BVbiY/+WdysxQ= - dependencies: - through2 "~2.0.0" - split@0.3: version "0.3.3" resolved "https://registry.npmjs.org/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" @@ -19293,11 +17043,6 @@ stack-trace@0.0.x: resolved "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= -stack-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" - integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== - stack-utils@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593" @@ -19384,14 +17129,6 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-combiner2@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" - integrity sha1-+02KFCDqNidk4hrUeAOXvry0HL4= - dependencies: - duplexer2 "~0.1.0" - readable-stream "^2.0.2" - stream-combiner@~0.0.4: version "0.0.4" resolved "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" @@ -19418,14 +17155,6 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" -stream-iterate@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/stream-iterate/-/stream-iterate-1.2.0.tgz#2bd7c77296c1702a46488b8ad41f79865eecd4e1" - integrity sha1-K9fHcpbBcCpGSIuK1B95hl7s1OE= - dependencies: - readable-stream "^2.1.5" - stream-shift "^1.0.0" - stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" @@ -19441,11 +17170,6 @@ strict-uri-encode@^1.0.0: resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= -strict-uri-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" - integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= - string-argv@0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" @@ -19461,14 +17185,6 @@ string-hash@^1.1.1: resolved "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= -string-length@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz#107ef8c23456e187a8abd4a61162ff4ac6e25837" - integrity sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA== - dependencies: - astral-regex "^1.0.0" - strip-ansi "^5.2.0" - string-length@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" @@ -19486,7 +17202,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -19563,11 +17279,6 @@ string_decoder@^1.0.0, string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -19584,11 +17295,6 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -stringify-package@^1.0.0, stringify-package@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" - integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== - strip-ansi@5.2.0, strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" @@ -19617,11 +17323,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-bom@4.0.0, strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -19634,6 +17335,11 @@ strip-bom@^3.0.0: resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -19663,10 +17369,10 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@3.0.1, strip-json-comments@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" - integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== +strip-json-comments@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" + integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== strip-json-comments@~2.0.1: version "2.0.1" @@ -19896,7 +17602,7 @@ tar-stream@^2.0.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^4, tar@^4.4.10, tar@^4.4.12, tar@^4.4.13, tar@^4.4.8: +tar@^4, tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: version "4.4.13" resolved "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== @@ -19945,11 +17651,6 @@ temp-dir@^1.0.0: resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= -temp-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" - integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== - temp-write@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492" @@ -19962,23 +17663,6 @@ temp-write@^3.4.0: temp-dir "^1.0.0" uuid "^3.0.1" -tempy@^0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/tempy/-/tempy-0.5.0.tgz#2785c89df39fcc4d1714fc554813225e1581d70b" - integrity sha512-VEY96x7gbIRfsxqsafy2l5yVxxp3PhwAGoWMyC2D2Zt5DmEv+2tGiPOrquNRpf21hhGnKLVEsuqleqiZmKG/qw== - dependencies: - is-stream "^2.0.0" - temp-dir "^2.0.0" - type-fest "^0.12.0" - unique-string "^2.0.0" - -term-size@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" - integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= - dependencies: - execa "^0.7.0" - term-size@^2.1.0: version "2.2.0" resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" @@ -20055,7 +17739,7 @@ text-hex@1.0.x: resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== -text-table@0.2.0, text-table@^0.2.0, text-table@~0.2.0: +text-table@0.2.0, text-table@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= @@ -20101,7 +17785,7 @@ throttleit@^1.0.0: resolved "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= -through2@^2.0.0, through2@^2.0.2, through2@~2.0.0: +through2@^2.0.0, through2@^2.0.2: version "2.0.5" resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -20136,11 +17820,6 @@ timeago.js@^4.0.2: resolved "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz#724e8c8833e3490676c7bb0a75f5daf20e558028" integrity sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w== -timed-out@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" - integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= - timers-browserify@^2.0.4: version "2.0.11" resolved "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" @@ -20163,11 +17842,6 @@ tiny-invariant@^1.0.2, tiny-invariant@^1.0.4, tiny-invariant@^1.0.6: resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== -tiny-relative-date@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" - integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== - tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" @@ -20292,11 +17966,6 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" -traverse@~0.6.6: - version "0.6.6" - resolved "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" - integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= - trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -20359,22 +18028,6 @@ ts-invariant@^0.4.0: dependencies: tslib "^1.9.3" -ts-jest@^25.2.1: - version "25.2.1" - resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-25.2.1.tgz#49bf05da26a8b7fbfbc36b4ae2fcdc2fef35c85d" - integrity sha512-TnntkEEjuXq/Gxpw7xToarmHbAafgCaAzOpnajnFC6jI7oo1trMzAHA04eWpc3MhV6+yvhE8uUBAmN+teRJh0A== - dependencies: - bs-logger "0.x" - buffer-from "1.x" - fast-json-stable-stringify "2.x" - json5 "2.x" - lodash.memoize "4.x" - make-error "1.x" - mkdirp "0.x" - resolve "1.x" - semver "^5.5" - yargs-parser "^16.1.0" - ts-jest@^26.0.0: version "26.0.0" resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-26.0.0.tgz#957b802978249aaf74180b9dcb17b4fd787ad6f3" @@ -20473,6 +18126,13 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -20490,11 +18150,6 @@ type-fest@^0.11.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== -type-fest@^0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz#f57a27ab81c68d136a51fd71467eff94157fa1ee" - integrity sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg== - type-fest@^0.3.0: version "0.3.1" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -20535,7 +18190,7 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.7.4, typescript@^3.9.2: +typescript@^3.9.2, typescript@^3.9.3: version "3.9.3" resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== @@ -20563,7 +18218,7 @@ uid2@0.0.x: resolved "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I= -umask@^1.1.0, umask@~1.1.0: +umask@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= @@ -20662,13 +18317,6 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" -unique-string@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" - integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo= - dependencies: - crypto-random-string "^1.0.0" - unique-string@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" @@ -20766,32 +18414,11 @@ untildify@4.0.0: resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -unzip-response@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" - integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= - upath@^1.1.1, upath@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-notifier@^2.2.0, update-notifier@^2.3.0, update-notifier@^2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" - integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw== - dependencies: - boxen "^1.2.1" - chalk "^2.0.1" - configstore "^3.0.0" - import-lazy "^2.1.0" - is-ci "^1.0.10" - is-installed-globally "^0.1.0" - is-npm "^1.0.0" - latest-version "^3.0.0" - semver-diff "^2.0.0" - xdg-basedir "^3.0.0" - update-notifier@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz#4866b98c3bc5b5473c020b1250583628f9a328f3" @@ -20823,11 +18450,6 @@ urix@^0.1.0: resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= -url-join@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" - integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== - url-loader@^2.0.1: version "2.3.0" resolved "https://registry.npmjs.org/url-loader/-/url-loader-2.3.0.tgz#e0e2ef658f003efb8ca41b0f3ffbf76bab88658b" @@ -20846,13 +18468,6 @@ url-loader@^4.1.0: mime-types "^2.1.26" schema-utils "^2.6.5" -url-parse-lax@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" - integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= - dependencies: - prepend-http "^1.0.1" - url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" @@ -20904,11 +18519,6 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -util-extend@^1.0.1: - version "1.0.3" - resolved "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" - integrity sha1-p8IW0mdUUWljeztu3GypEZ4v+T8= - util-promisify@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz#3c2236476c4d32c5ff3c47002add7c13b9a82a53" @@ -20924,7 +18534,7 @@ util.promisify@1.0.0: define-properties "^1.1.2" object.getownpropertydescriptors "^2.0.3" -util.promisify@^1.0.0, util.promisify@~1.0.0: +util.promisify@~1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== @@ -20958,7 +18568,7 @@ utils-merge@1.0.1, utils-merge@1.x.x: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0: +uuid@^3.0.1, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -20978,15 +18588,6 @@ v8-compile-cache@^2.0.3: resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== -v8-to-istanbul@^4.0.1: - version "4.1.2" - resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.2.tgz#387d173be5383dbec209d21af033dcb892e3ac82" - integrity sha512-G9R+Hpw0ITAmPSr47lSlc5A1uekSYzXxTMlFxso2xoffwo4jQnzbv1p9yXIinO8UMZKfAFewaCHwWvnH4Jb4Ug== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^1.6.0" - source-map "^0.7.3" - v8-to-istanbul@^4.1.3: version "4.1.4" resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz#b97936f21c0e2d9996d4985e5c5156e9d4e49cd6" @@ -21008,7 +18609,7 @@ valid-url@1.0.9: resolved "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= -validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3, validate-npm-package-license@^3.0.4: +validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: version "3.0.4" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== @@ -21016,7 +18617,7 @@ validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3, valida spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0: +validate-npm-package-name@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= @@ -21086,15 +18687,6 @@ w3c-hr-time@^1.0.1, w3c-hr-time@^1.0.2: dependencies: browser-process-hrtime "^1.0.0" -w3c-xmlserializer@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794" - integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg== - dependencies: - domexception "^1.0.1" - webidl-conversions "^4.0.2" - xml-name-validator "^3.0.0" - w3c-xmlserializer@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" @@ -21363,7 +18955,7 @@ which-pm-runs@^1.0.0: resolved "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= -which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: +which@^1.2.14, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -21384,13 +18976,6 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" -widest-line@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" - integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== - dependencies: - string-width "^2.1.1" - widest-line@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" @@ -21428,7 +19013,7 @@ winston@^3.2.1: triple-beam "^1.3.0" winston-transport "^4.3.0" -word-wrap@^1.0.3, word-wrap@~1.2.3: +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -21438,7 +19023,7 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -worker-farm@^1.6.0, worker-farm@^1.7.0: +worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== @@ -21452,14 +19037,6 @@ worker-rpc@^0.1.0: dependencies: microevent.ts "~0.1.1" -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" @@ -21491,7 +19068,7 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^2.0.0, write-file-atomic@^2.3.0, write-file-atomic@^2.4.2, write-file-atomic@^2.4.3: +write-file-atomic@^2.0.0, write-file-atomic@^2.3.0, write-file-atomic@^2.4.2: version "2.4.3" resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== @@ -21563,7 +19140,7 @@ ws@^6.1.2, ws@^6.2.1: dependencies: async-limiter "~1.0.0" -ws@^7.0.0, ws@^7.2.3: +ws@^7.2.3: version "7.3.0" resolved "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== @@ -21578,11 +19155,6 @@ x-xss-protection@1.3.0: resolved "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz#3e3a8dd638da80421b0e9fff11a2dbe168f6d52c" integrity sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg== -xdg-basedir@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" - integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= - xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" @@ -21593,12 +19165,7 @@ xml-name-validator@^3.0.0: resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" - integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= - -xmlchars@^2.1.1, xmlchars@^2.2.0: +xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== @@ -21615,11 +19182,6 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= - y18n@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" @@ -21685,14 +19247,6 @@ yargs-parser@^15.0.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^16.1.0: - version "16.1.0" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1" - integrity sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^18.1.1: version "18.1.1" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.1.tgz#bf7407b915427fc760fcbbccc6c82b4f0ffcbd37" @@ -21701,38 +19255,6 @@ yargs-parser@^18.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" - integrity sha1-jQrELxbqVd69MyyvTEA4s+P139k= - dependencies: - camelcase "^4.1.0" - -yargs-parser@^9.0.2: - version "9.0.2" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" - integrity sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc= - dependencies: - camelcase "^4.1.0" - -yargs@^11.0.0: - version "11.1.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-11.1.1.tgz#5052efe3446a4df5ed669c995886cc0f13702766" - integrity sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw== - dependencies: - cliui "^4.0.0" - decamelize "^1.1.1" - find-up "^2.1.0" - get-caller-file "^1.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^9.0.2" - yargs@^13.3.2: version "13.3.2" resolved "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" @@ -21766,7 +19288,7 @@ yargs@^14.2.2: y18n "^4.0.0" yargs-parser "^15.0.1" -yargs@^15.0.0, yargs@^15.0.1, yargs@^15.3.1: +yargs@^15.3.1: version "15.3.1" resolved "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA== @@ -21783,25 +19305,6 @@ yargs@^15.0.0, yargs@^15.0.1, yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.1" -yargs@^8.0.2: - version "8.0.2" - resolved "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" - integrity sha1-YpmpBVsc78lp/355wdkY3Osiw2A= - dependencies: - camelcase "^4.1.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^2.0.0" - read-pkg-up "^2.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^7.0.0" - yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From 9f50e7c1b46e07179a8a296276545cab6a63149e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 May 2020 11:33:53 +0200 Subject: [PATCH 69/97] packages,plugins: fix lint issues --- packages/backend/src/plugins/auth.ts | 2 +- packages/backend/src/plugins/catalog.ts | 5 ++- packages/backend/src/plugins/identity.ts | 2 +- packages/backend/src/plugins/scaffolder.ts | 2 +- packages/backend/src/plugins/sentry.ts | 2 +- .../core-api/src/app/AppThemeProvider.tsx | 10 ++--- .../FeatureCalloutCircular.tsx | 4 +- .../FeatureDiscovery/lib/usePortal.ts | 41 ++++++++++--------- .../FeatureDiscovery/lib/useShowCallout.ts | 2 +- .../ProgressBars/HorizontalProgress.tsx | 2 +- .../ComponentPage/ComponentPage.test.tsx | 13 +++++- .../ComponentPage/ComponentPage.tsx | 12 +++--- .../src/components/Settings/Settings.tsx | 2 +- .../BuildWithStepsPage/BuildWithStepsPage.tsx | 2 +- .../circleci/src/state/useBuildWithSteps.ts | 2 +- plugins/circleci/src/state/useBuilds.ts | 4 +- plugins/circleci/src/state/useSettings.ts | 40 +++++++++--------- .../src/components/AuditList/index.tsx | 2 +- .../SentryPluginWidget/SentryPluginWidget.tsx | 2 +- .../src/components/RadarComponent.tsx | 20 ++++----- 20 files changed, 93 insertions(+), 78 deletions(-) diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index c2e349f640..7cf9610dc2 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -17,6 +17,6 @@ import { createRouter } from '@backstage/plugin-auth-backend'; import { PluginEnvironment } from '../types'; -export default async function ({ logger }: PluginEnvironment) { +export default async function createPlugin({ logger }: PluginEnvironment) { return await createRouter({ logger }); } diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 9f5315fa08..91cbd8c254 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -27,7 +27,10 @@ import { import { PluginEnvironment } from '../types'; import { EntityPolicies } from '@backstage/catalog-model'; -export default async function ({ logger, database }: PluginEnvironment) { +export default async function createPlugin({ + logger, + database, +}: PluginEnvironment) { const policy = new EntityPolicies(); const ingestion = new IngestionModels( new LocationReaders(), diff --git a/packages/backend/src/plugins/identity.ts b/packages/backend/src/plugins/identity.ts index 26276afd01..63a326965c 100644 --- a/packages/backend/src/plugins/identity.ts +++ b/packages/backend/src/plugins/identity.ts @@ -17,6 +17,6 @@ import { createRouter } from '@backstage/plugin-identity-backend'; import { PluginEnvironment } from '../types'; -export default async function ({ logger }: PluginEnvironment) { +export default async function createPlugin({ logger }: PluginEnvironment) { return await createRouter({ logger }); } diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 311c9197aa..08e700bc74 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -21,7 +21,7 @@ import { } from '@backstage/plugin-scaffolder-backend'; import type { PluginEnvironment } from '../types'; -export default async function ({ logger }: PluginEnvironment) { +export default async function createPlugin({ logger }: PluginEnvironment) { const storage = new DiskStorage({ logger }); const templater = new CookieCutter(); diff --git a/packages/backend/src/plugins/sentry.ts b/packages/backend/src/plugins/sentry.ts index 34506ee3de..89ee153faf 100644 --- a/packages/backend/src/plugins/sentry.ts +++ b/packages/backend/src/plugins/sentry.ts @@ -17,6 +17,6 @@ import { createRouter } from '@backstage/plugin-sentry-backend'; import { Logger } from 'winston'; -export default async function (logger: Logger) { +export default async function createPlugin(logger: Logger) { return await createRouter(logger); } diff --git a/packages/core-api/src/app/AppThemeProvider.tsx b/packages/core-api/src/app/AppThemeProvider.tsx index 775d8293ba..6bbcaea93a 100644 --- a/packages/core-api/src/app/AppThemeProvider.tsx +++ b/packages/core-api/src/app/AppThemeProvider.tsx @@ -49,10 +49,6 @@ function resolveTheme( } const useShouldPreferDarkTheme = () => { - if (!window.matchMedia) { - return false; - } - const mediaQuery = useMemo( () => window.matchMedia('(prefers-color-scheme: dark)'), [], @@ -74,12 +70,16 @@ const useShouldPreferDarkTheme = () => { export const AppThemeProvider: FC<{}> = ({ children }) => { const appThemeApi = useApi(appThemeApiRef); - const shouldPreferDark = useShouldPreferDarkTheme(); const themeId = useObservable( appThemeApi.activeThemeId$(), appThemeApi.getActiveThemeId(), ); + // Browser feature detection won't change over time, so ignore lint rule + const shouldPreferDark = Boolean(window.matchMedia) + ? useShouldPreferDarkTheme() // eslint-disable-line react-hooks/rules-of-hooks + : false; + const appTheme = resolveTheme( themeId, shouldPreferDark, diff --git a/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx b/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx index a3b40d5b88..38e422dbcc 100644 --- a/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx +++ b/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx @@ -141,9 +141,9 @@ export const FeatureCalloutCircular: FC = ({ window.removeEventListener('resize', update); window.removeEventListener('scroll', update); }; - }, []); + }, [update]); - useLayoutEffect(update, [wrapperRef.current]); + useLayoutEffect(update, [wrapperRef.current, update]); if (!show) { return <>{children}; diff --git a/packages/core/src/components/FeatureDiscovery/lib/usePortal.ts b/packages/core/src/components/FeatureDiscovery/lib/usePortal.ts index 6031a25f1c..d5fd2c23c9 100644 --- a/packages/core/src/components/FeatureDiscovery/lib/usePortal.ts +++ b/packages/core/src/components/FeatureDiscovery/lib/usePortal.ts @@ -51,27 +51,30 @@ function addRootElement(rootElem: Element): void { export function usePortal(id: string): HTMLElement { const rootElemRef = useRef(null); - useEffect(function setupElement() { - // Look for existing target dom element to append to - const existingParent = document.querySelector(`#${id}`); - // Parent is either a new root or the existing dom element - const parentElem = existingParent || createRootElement(id); + useEffect( + function setupElement() { + // Look for existing target dom element to append to + const existingParent = document.querySelector(`#${id}`); + // Parent is either a new root or the existing dom element + const parentElem = existingParent || createRootElement(id); - // If there is no existing DOM element, add a new one. - if (!existingParent) { - addRootElement(parentElem); - } - - // Add the detached element to the parent - parentElem.appendChild(rootElemRef.current!); - - return function removeElement() { - rootElemRef.current!.remove(); - if (parentElem.childNodes.length === -1) { - parentElem.remove(); + // If there is no existing DOM element, add a new one. + if (!existingParent) { + addRootElement(parentElem); } - }; - }, []); + + // Add the detached element to the parent + parentElem.appendChild(rootElemRef.current!); + + return function removeElement() { + rootElemRef.current!.remove(); + if (parentElem.childNodes.length === -1) { + parentElem.remove(); + } + }; + }, + [id], + ); /** * It's important we evaluate this lazily: diff --git a/packages/core/src/components/FeatureDiscovery/lib/useShowCallout.ts b/packages/core/src/components/FeatureDiscovery/lib/useShowCallout.ts index 047473a7e2..0bbcf3b8ec 100644 --- a/packages/core/src/components/FeatureDiscovery/lib/useShowCallout.ts +++ b/packages/core/src/components/FeatureDiscovery/lib/useShowCallout.ts @@ -45,7 +45,7 @@ function useCalloutHasBeenSeen( const markSeen = useCallback(() => { setState(featureId, true); - }, [featureId]); + }, [setState, featureId]); return { seen: states[featureId] === true, markSeen }; } diff --git a/packages/core/src/components/ProgressBars/HorizontalProgress.tsx b/packages/core/src/components/ProgressBars/HorizontalProgress.tsx index 7575c5a9b2..d7f813f55f 100644 --- a/packages/core/src/components/ProgressBars/HorizontalProgress.tsx +++ b/packages/core/src/components/ProgressBars/HorizontalProgress.tsx @@ -29,6 +29,7 @@ type Props = { }; const HorizontalProgress: FC = ({ value }) => { + const theme = useTheme(); if (isNaN(value)) { return null; } @@ -36,7 +37,6 @@ const HorizontalProgress: FC = ({ value }) => { if (percent > 100) { percent = 100; } - const theme = useTheme(); const strokeColor = getProgressColor(theme.palette, percent, false, 100); return ( diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index e21c4dbce9..f580a84d8a 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -18,6 +18,7 @@ import { render } from '@testing-library/react'; import * as React from 'react'; import { wrapInTestApp } from '@backstage/test-utils'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { catalogApiRef, CatalogApi } from '../../api/types'; const getTestProps = (componentName: string) => { return { @@ -39,7 +40,17 @@ describe('ComponentPage', () => { const props = getTestProps(''); await render( wrapInTestApp( - + , ), diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx index 0ee0e1d09a..62f34f1620 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -53,11 +53,6 @@ const ComponentPage: FC = ({ match, history }) => { const componentName = match.params.name; const errorApi = useApi(errorApiRef); - if (componentName === '') { - history.push('/catalog'); - return null; - } - const catalogApi = useApi(catalogApiRef); const catalogRequest = useAsync(() => catalogApi.getEntityByName(match.params.name), @@ -70,7 +65,12 @@ const ComponentPage: FC = ({ match, history }) => { history.push('/catalog'); }, REDIRECT_DELAY); } - }, [catalogRequest.error]); + }, [catalogRequest.error, errorApi, history]); + + if (componentName === '') { + history.push('/catalog'); + return null; + } const removeComponent = async () => { setConfirmationDialogOpen(false); diff --git a/plugins/circleci/src/components/Settings/Settings.tsx b/plugins/circleci/src/components/Settings/Settings.tsx index f0ed3f2ceb..8897c1eb40 100644 --- a/plugins/circleci/src/components/Settings/Settings.tsx +++ b/plugins/circleci/src/components/Settings/Settings.tsx @@ -52,7 +52,7 @@ const Settings = () => { if (repoFromStore !== repo) { setRepo(repoFromStore); } - }, [ownerFromStore, repoFromStore, tokenFromStore]); + }, [ownerFromStore, repoFromStore, tokenFromStore, token, owner, repo]); const [saved, setSaved] = useState(false); diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx index 75d09610b5..77e6daf755 100644 --- a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -111,7 +111,7 @@ const BuildWithStepsView: FC<{}> = () => { useEffect(() => { startPolling(); return () => stopPolling(); - }, [buildId, settings]); + }, [buildId, settings, startPolling, stopPolling]); return ( <> diff --git a/plugins/circleci/src/state/useBuildWithSteps.ts b/plugins/circleci/src/state/useBuildWithSteps.ts index 7aba770851..8fa7fa896d 100644 --- a/plugins/circleci/src/state/useBuildWithSteps.ts +++ b/plugins/circleci/src/state/useBuildWithSteps.ts @@ -46,7 +46,7 @@ export function useBuildWithSteps(buildId: number) { errorApi.post(e); return Promise.reject(e); } - }, [token, owner, repo, buildId]); + }, [token, owner, repo, buildId, api, errorApi]); const restartBuild = async () => { try { diff --git a/plugins/circleci/src/state/useBuilds.ts b/plugins/circleci/src/state/useBuilds.ts index f90311b56b..6d38c5f901 100644 --- a/plugins/circleci/src/state/useBuilds.ts +++ b/plugins/circleci/src/state/useBuilds.ts @@ -101,7 +101,7 @@ export function useBuilds() { return Promise.reject(e); } }, - [repo, token, owner], + [repo, token, owner, api, errorApi], ); const restartBuild = async (buildId: number) => { @@ -121,7 +121,7 @@ export function useBuilds() { useEffect(() => { getBuilds({ limit: 1, offset: 0 }).then(b => setTotal(b?.[0].build_num!)); - }, [repo]); + }, [repo, getBuilds]); const { loading, value, retry } = useAsyncRetry( () => diff --git a/plugins/circleci/src/state/useSettings.ts b/plugins/circleci/src/state/useSettings.ts index c8dc9ce39b..3cc58a65bd 100644 --- a/plugins/circleci/src/state/useSettings.ts +++ b/plugins/circleci/src/state/useSettings.ts @@ -23,27 +23,29 @@ export function useSettings() { const errorApi = useApi(errorApiRef); - const rehydrate = () => { - try { - const stateFromStorage = JSON.parse(sessionStorage.getItem(STORAGE_KEY)!); - if ( - stateFromStorage && - Object.keys(stateFromStorage).some( - k => (settings as any)[k] !== stateFromStorage[k], - ) - ) - dispatch({ - type: 'setCredentials', - payload: stateFromStorage, - }); - } catch (error) { - errorApi.post(error); - } - }; - useEffect(() => { + const rehydrate = () => { + try { + const stateFromStorage = JSON.parse( + sessionStorage.getItem(STORAGE_KEY)!, + ); + if ( + stateFromStorage && + Object.keys(stateFromStorage).some( + k => (settings as any)[k] !== stateFromStorage[k], + ) + ) + dispatch({ + type: 'setCredentials', + payload: stateFromStorage, + }); + } catch (error) { + errorApi.post(error); + } + }; + rehydrate(); - }, []); + }, [dispatch, errorApi, settings]); const persist = (state: Settings) => { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); diff --git a/plugins/lighthouse/src/components/AuditList/index.tsx b/plugins/lighthouse/src/components/AuditList/index.tsx index 85be31cc80..4dbbb05295 100644 --- a/plugins/lighthouse/src/components/AuditList/index.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.tsx @@ -63,7 +63,7 @@ const AuditList: FC<{}> = () => { if (value?.total && value?.limit) return Math.ceil(value?.total / value?.limit); return 0; - }, [value]); + }, [value?.total, value?.limit]); const history = useHistory(); diff --git a/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx b/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx index 5049905966..b101a9092a 100644 --- a/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx +++ b/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx @@ -43,7 +43,7 @@ export const SentryPluginWidget: FC<{ if (error) { errorApi.post(error); } - }, [error]); + }, [error, errorApi]); if (loading) { return ( diff --git a/plugins/tech-radar/src/components/RadarComponent.tsx b/plugins/tech-radar/src/components/RadarComponent.tsx index 7a6c4fedaf..981d1f1369 100644 --- a/plugins/tech-radar/src/components/RadarComponent.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.tsx @@ -21,6 +21,7 @@ import { TechRadarComponentProps, TechRadarLoaderResponse } from '../api'; import getSampleData from '../sampleData'; const useTechRadarLoader = (props: TechRadarComponentProps) => { + const errorApi = useApi(errorApiRef); const [state, setState] = useState<{ loading: boolean; error?: Error; @@ -31,38 +32,33 @@ const useTechRadarLoader = (props: TechRadarComponentProps) => { data: undefined, }); + const { getData } = props; + useEffect(() => { - if (!props.getData) { + if (!getData) { return; } - props - .getData() + getData() .then((payload: TechRadarLoaderResponse) => { setState({ loading: false, error: undefined, data: payload }); }) .catch((err: Error) => { + errorApi.post(err); setState({ loading: false, error: err, data: undefined, }); }); - }, []); + }, [getData, errorApi]); return state; }; -const RadarComponent: FC = (props) => { - const errorApi = useApi(errorApiRef); +const RadarComponent: FC = props => { const { loading, error, data } = useTechRadarLoader(props); - useEffect(() => { - if (error) { - errorApi.post(error); - } - }, [error && error.message]); - return ( <> {loading && } From 7da845e844e9c2b81e2942d0c6a394f7ac933abb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 May 2020 11:58:10 +0200 Subject: [PATCH 70/97] packages/cli: use own tsconfig --- packages/cli/config/tsconfig.json | 32 +++++++++++++++++++++++++------ packages/cli/package.json | 1 - tsconfig.json | 1 - yarn.lock | 5 ----- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/cli/config/tsconfig.json b/packages/cli/config/tsconfig.json index fb52420860..9f02bfcafe 100644 --- a/packages/cli/config/tsconfig.json +++ b/packages/cli/config/tsconfig.json @@ -1,17 +1,37 @@ { - "extends": "@spotify/tsconfig", - "exclude": ["**/*.test.*"], "compilerOptions": { "allowJs": true, - "noEmit": false, + "declaration": true, + "declarationMap": false, "emitDeclarationOnly": true, + "esModuleInterop": true, + "experimentalDecorators": false, + "forceConsistentCasingInFileNames": true, + "importHelpers": false, "incremental": true, - "target": "ES2019", + "isolatedModules": false, + "jsx": "react", + "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2019"], "module": "ESNext", + "moduleResolution": "node", + "noEmit": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "pretty": true, "removeComments": false, "resolveJsonModule": true, - "esModuleInterop": true, - "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2019"], + "sourceMap": false, + "strict": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2019", "types": ["node", "jest"] } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 13c0fd4aa3..20d9893a37 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,7 +36,6 @@ "@rollup/plugin-json": "^4.0.2", "@rollup/plugin-node-resolve": "^7.1.1", "@spotify/eslint-config": "^7.0.1", - "@spotify/tsconfig": "^7.0.0", "@sucrase/webpack-loader": "^2.0.0", "bfj": "^7.0.2", "chalk": "^4.0.0", diff --git a/tsconfig.json b/tsconfig.json index 52cdd1e825..2b36617ad9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "@backstage/cli/config/tsconfig.json", "include": ["packages/*/src", "plugins/*/src", "plugins/*/dev"], - "exclude": ["**/node_modules"], "compilerOptions": { "outDir": "dist" } diff --git a/yarn.lock b/yarn.lock index 679f75cb36..74fba61f49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2540,11 +2540,6 @@ resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-7.0.0.tgz#47750979d1282197295108b6958360660a955c16" integrity sha512-lIMcx/2oDqTtW84iHKkRJe+8U6HK6GPwWH5sJp9UEHcDpdXomOQYvwcGXy2I2zwPQQ14gYYE6nEJuSnnYqsYRw== -"@spotify/tsconfig@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/@spotify/tsconfig/-/tsconfig-7.0.0.tgz#41c402f4eb6d3147bc18427a35205151cbb32cd5" - integrity sha512-MeRFUPMXWBSm6yaUWiESaQsF9B+9Rn1F/w5hbHHzcunc45teXBcgsOrJu1uDOEkhP/9lP0fefuodpP+TYWM1LQ== - "@spotify/web-scripts-utils@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/@spotify/web-scripts-utils/-/web-scripts-utils-7.0.0.tgz#8c6b8039fc645a36ac48629eb9ba06600f4d828a" From f9e4e557f8bd5b81b28bfe5c19e76e8f4e720fb7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 May 2020 12:00:26 +0200 Subject: [PATCH 71/97] packages/cli: enable TS isolatedModules --- packages/catalog-model/src/kinds/index.ts | 7 ++++--- packages/catalog-model/src/setupTests.ts | 2 ++ packages/cli/config/tsconfig.json | 2 +- packages/cli/src/commands/lint.ts | 3 +-- .../src/providers/OAuthProvider.test.ts | 21 ------------------- .../auth-backend/src/providers/index.test.ts | 4 +++- 6 files changed, 11 insertions(+), 28 deletions(-) delete mode 100644 plugins/auth-backend/src/providers/OAuthProvider.test.ts diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index 97d22c14a5..ed79fed61d 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import type { ComponentV1beta1 } from './ComponentV1beta1'; +export type { + ComponentV1beta1, + ComponentV1beta1 as Component, +} from './ComponentV1beta1'; export { ComponentV1beta1Policy } from './ComponentV1beta1'; -export { ComponentV1beta1 as Component }; -export { ComponentV1beta1 }; diff --git a/packages/catalog-model/src/setupTests.ts b/packages/catalog-model/src/setupTests.ts index f3b69cc361..ba33cf996b 100644 --- a/packages/catalog-model/src/setupTests.ts +++ b/packages/catalog-model/src/setupTests.ts @@ -13,3 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export {}; diff --git a/packages/cli/config/tsconfig.json b/packages/cli/config/tsconfig.json index 9f02bfcafe..f6d622ee16 100644 --- a/packages/cli/config/tsconfig.json +++ b/packages/cli/config/tsconfig.json @@ -9,7 +9,7 @@ "forceConsistentCasingInFileNames": true, "importHelpers": false, "incremental": true, - "isolatedModules": false, + "isolatedModules": true, "jsx": "react", "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2019"], "module": "ESNext", diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 7eeedcbe29..41e69c51be 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -20,8 +20,7 @@ import { paths } from '../lib/paths'; export default async (cmd: Command) => { const args = [ - '--ext', - 'js,jsx,ts,tsx', + '--ext=js,jsx,ts,tsx', '--max-warnings=0', '--format=codeframe', paths.targetDir, diff --git a/plugins/auth-backend/src/providers/OAuthProvider.test.ts b/plugins/auth-backend/src/providers/OAuthProvider.test.ts deleted file mode 100644 index 308849057b..0000000000 --- a/plugins/auth-backend/src/providers/OAuthProvider.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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. - */ - -describe('OAuthProvider', () => { - it('unbreak test runner', () => { - expect(true).toBeTruthy(); - }); -}); diff --git a/plugins/auth-backend/src/providers/index.test.ts b/plugins/auth-backend/src/providers/index.test.ts index b3e2f19771..7f39d9de57 100644 --- a/plugins/auth-backend/src/providers/index.test.ts +++ b/plugins/auth-backend/src/providers/index.test.ts @@ -14,8 +14,10 @@ * limitations under the License. */ +import { defaultRouter } from '.'; + describe('test', () => { it('unbreaks the test runner', () => { - expect(true).toBeTruthy(); + expect(defaultRouter).toBeDefined(); }); }); From 067d44181bc1d907514c2aa763d48b52e7f50782 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 1 Jun 2020 17:00:50 +0200 Subject: [PATCH 72/97] scripts/check-type-dependencies: only verify type deps for packages that have been built --- scripts/check-type-dependencies.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/check-type-dependencies.js b/scripts/check-type-dependencies.js index 5b7daa13d2..6ee4626a21 100755 --- a/scripts/check-type-dependencies.js +++ b/scripts/check-type-dependencies.js @@ -69,7 +69,11 @@ async function main() { } function shouldCheckTypes(pkg) { - return !pkg.private && pkg.get('types'); + return ( + !pkg.private && + pkg.get('types') && + fs.existsSync(resolvePath(pkg.location, 'dist/index.d.ts')) + ); } /** From 2ee6f5d87fb96b9ab929df38689a3f7cbed5be34 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 1 Jun 2020 17:02:11 +0200 Subject: [PATCH 73/97] github/workflows: remove scripts from cli deps --- .github/workflows/cli.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 07ed78743c..24ff2a734c 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -6,7 +6,6 @@ on: - '.github/workflows/cli.yml' - 'packages/cli/**' - 'packages/core/**' - - 'scripts/**' - 'yarn.lock' jobs: From 37ca9e1434fb8fe31d463457fac1bec7d9142948 Mon Sep 17 00:00:00 2001 From: Marc Bruggmann Date: Mon, 1 Jun 2020 17:06:00 +0200 Subject: [PATCH 74/97] Clarify that we start by only implementing the Component entity --- docs/architecture-decisions/adr005-catalog-core-entities.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md index f14f8c8179..1014019f8a 100644 --- a/docs/architecture-decisions/adr005-catalog-core-entities.md +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -10,7 +10,7 @@ We want to standardize on a few core entities that we are tracking in the Backst ## Decision -We maintain a catalog of the following core entities: +Backstage should eventually support the following core entities: * **Components** are individual pieces of software * **APIs** are the boundaries between different components @@ -18,6 +18,8 @@ We maintain a catalog of the following core entities: ![Catalog Core Entities][catalog-core-entities] +For now, we'll start by only implementing support for the Component entity in the Backstage catalog. This can later be extended to APIs, Resources and other potentially useful entities. + ### Component A component is a piece of software, for example a mobile application feature, web site, backend service or data pipeline (list not exhaustive). A component can be tracked in source control, or use some existing open source or commercial software. It can implement APIs for other components to consume. In turn it might depend on APIs implemented by other components, or resources that are attached to it at runtime. @@ -72,4 +74,4 @@ spec: ## Consequences -We will start with fleshing out support for the Component entity in the catalog, and expand to APIs and Resources later down the line. +We will continue fleshing out support for the Component entity in the Backstage catalog. From 5094dc75a51f973f1b635397813651bce18b5fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 1 Jun 2020 21:27:38 +0200 Subject: [PATCH 75/97] Add ability to specify locationId on addOrUpdateEntity --- .../src/catalog/DatabaseEntitiesCatalog.ts | 14 ++++++++++---- plugins/catalog-backend/src/catalog/types.ts | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index d13ac9a1ba..606b6dc475 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -49,13 +49,16 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { ); } - async addOrUpdateEntity(entity: Entity): Promise { + async addOrUpdateEntity( + entity: Entity, + locationId?: string, + ): Promise { await this.policy.enforce(entity); return await this.database.transaction(async tx => { let response: DbEntityResponse; if (entity.metadata.uid) { - response = await this.database.updateEntity(tx, { entity }); + response = await this.database.updateEntity(tx, { locationId, entity }); } else { const existing = await this.entityByNameInternal( tx, @@ -64,9 +67,12 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { entity.metadata.namespace, ); if (existing) { - response = await this.database.updateEntity(tx, { entity }); + response = await this.database.updateEntity(tx, { + locationId, + entity, + }); } else { - response = await this.database.addEntity(tx, { entity }); + response = await this.database.addEntity(tx, { locationId, entity }); } } diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 61fba33ebf..2ef5fd9e56 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -30,7 +30,7 @@ export type EntitiesCatalog = { namespace: string | undefined, name: string, ): Promise; - addOrUpdateEntity(entity: Entity): Promise; + addOrUpdateEntity(entity: Entity, locationId?: string): Promise; removeEntityByUid(uid: string): Promise; }; From f7ef6d492147449ecb9538f3166789c0df6d9580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 2 Jun 2020 09:32:45 +0200 Subject: [PATCH 76/97] Move addLocation to new HigherOrderOperations --- packages/backend/src/plugins/catalog.ts | 21 +- .../catalog/DatabaseEntitiesCatalog.test.ts | 13 +- .../src/catalog/DatabaseEntitiesCatalog.ts | 8 +- .../catalog/DatabaseLocationsCatalog.test.ts | 72 ++---- .../src/catalog/DatabaseLocationsCatalog.ts | 32 +-- plugins/catalog-backend/src/catalog/index.ts | 3 +- plugins/catalog-backend/src/catalog/types.ts | 22 +- .../src/database/CommonDatabase.test.ts | 47 ++-- .../src/database/CommonDatabase.ts | 28 +-- .../migrations/20200511113813_init.ts | 6 +- plugins/catalog-backend/src/database/types.ts | 16 +- .../src/ingestion/HigherOrderOperations.ts | 119 ++++++++++ .../catalog-backend/src/ingestion/index.ts | 1 + .../catalog-backend/src/ingestion/types.ts | 11 + .../src/service/router.test.ts | 208 +++++++----------- plugins/catalog-backend/src/service/router.ts | 31 ++- 16 files changed, 324 insertions(+), 314 deletions(-) create mode 100644 plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 91cbd8c254..150b9d1826 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -23,6 +23,7 @@ import { LocationReaders, IngestionModels, runPeriodically, + HigherOrderOperations, } from '@backstage/plugin-catalog-backend'; import { PluginEnvironment } from '../types'; import { EntityPolicies } from '@backstage/catalog-model'; @@ -32,7 +33,7 @@ export default async function createPlugin({ database, }: PluginEnvironment) { const policy = new EntityPolicies(); - const ingestion = new IngestionModels( + const ingestionModel = new IngestionModels( new LocationReaders(), new DescriptorParsers(), new EntityPolicies(), @@ -40,12 +41,22 @@ export default async function createPlugin({ const db = await DatabaseManager.createDatabase(database, logger); runPeriodically( - () => DatabaseManager.refreshLocations(db, ingestion, policy, logger), + () => DatabaseManager.refreshLocations(db, ingestionModel, policy, logger), 10000, ); - const entitiesCatalog = new DatabaseEntitiesCatalog(db, policy); - const locationsCatalog = new DatabaseLocationsCatalog(db, ingestion); + const entitiesCatalog = new DatabaseEntitiesCatalog(db); + const locationsCatalog = new DatabaseLocationsCatalog(db); + const higherOrderOperation = new HigherOrderOperations( + entitiesCatalog, + locationsCatalog, + ingestionModel, + ); - return await createRouter({ entitiesCatalog, locationsCatalog, logger }); + return await createRouter({ + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + logger, + }); } diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts index 1de5038112..b185075002 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -14,13 +14,12 @@ * limitations under the License. */ -import type { Entity, EntityPolicy } from '@backstage/catalog-model'; +import type { Entity } from '@backstage/catalog-model'; import type { Database } from '../database'; import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; describe('DatabaseEntitiesCatalog', () => { let db: jest.Mocked; - let policy: EntityPolicy; beforeEach(() => { db = { @@ -38,7 +37,6 @@ describe('DatabaseEntitiesCatalog', () => { addLocationUpdateLogEvent: jest.fn(), }; db.transaction.mockImplementation(async f => f('tx')); - policy = { enforce: jest.fn(async x => x) }; }); describe('addOrUpdateEntity', () => { @@ -55,10 +53,9 @@ describe('DatabaseEntitiesCatalog', () => { db.entities.mockResolvedValue([]); db.addEntity.mockResolvedValue({ entity }); - const catalog = new DatabaseEntitiesCatalog(db, policy); + const catalog = new DatabaseEntitiesCatalog(db); const result = await catalog.addOrUpdateEntity(entity); - expect(policy.enforce).toBeCalledWith(entity); expect(db.entities).toHaveBeenCalledTimes(1); expect(db.addEntity).toHaveBeenCalledTimes(1); expect(result).toBe(entity); @@ -78,10 +75,9 @@ describe('DatabaseEntitiesCatalog', () => { db.entities.mockResolvedValue([]); db.updateEntity.mockResolvedValue({ entity }); - const catalog = new DatabaseEntitiesCatalog(db, policy); + const catalog = new DatabaseEntitiesCatalog(db); const result = await catalog.addOrUpdateEntity(entity); - expect(policy.enforce).toBeCalledWith(entity); expect(db.entities).toHaveBeenCalledTimes(0); expect(db.updateEntity).toHaveBeenCalledTimes(1); expect(result).toBe(entity); @@ -108,10 +104,9 @@ describe('DatabaseEntitiesCatalog', () => { db.entities.mockResolvedValue([{ entity: existing }]); db.updateEntity.mockResolvedValue({ entity: added }); - const catalog = new DatabaseEntitiesCatalog(db, policy); + const catalog = new DatabaseEntitiesCatalog(db); const result = await catalog.addOrUpdateEntity(added); - expect(policy.enforce).toBeCalledWith(added); expect(db.entities).toHaveBeenCalledTimes(1); expect(db.updateEntity).toHaveBeenCalledTimes(1); expect(result).toEqual(existing); diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 606b6dc475..6a7a51a168 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -14,15 +14,12 @@ * limitations under the License. */ -import type { Entity, EntityPolicy } from '@backstage/catalog-model'; +import type { Entity } from '@backstage/catalog-model'; import type { Database, DbEntityResponse, EntityFilters } from '../database'; import type { EntitiesCatalog } from './types'; export class DatabaseEntitiesCatalog implements EntitiesCatalog { - constructor( - private readonly database: Database, - private readonly policy: EntityPolicy, - ) {} + constructor(private readonly database: Database) {} async entities(filters?: EntityFilters): Promise { const items = await this.database.transaction(tx => @@ -53,7 +50,6 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { entity: Entity, locationId?: string, ): Promise { - await this.policy.enforce(entity); return await this.database.transaction(async tx => { let response: DbEntityResponse; diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts index 443b3bf6b0..907bdea2d5 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -13,73 +13,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger } from '@backstage/backend-common'; -import type { Entity } from '@backstage/catalog-model'; import Knex from 'knex'; import path from 'path'; import { CommonDatabase } from '../database'; -import type { Database } from '../database'; -import type { IngestionModel } from '../ingestion/types'; import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; -class MockIngestionModel implements IngestionModel { - readLocation = jest.fn(async (type: string, target: string) => { - if (type !== 'valid_type') { - throw new Error(`Unknown location type ${type}`); - } - if (target === 'valid_target') { - return [{ type: 'data', data: {} as Entity } as const]; - } - throw new Error( - `Can't read location at ${target} with error: Something is broken`, - ); - }); -} - describe('DatabaseLocationsCatalog', () => { - const knex = Knex({ - client: 'sqlite3', - connection: ':memory:', - useNullAsDefault: true, - }); - knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => { - resource.run('PRAGMA foreign_keys = ON', () => {}); - }); - let db: Database; let catalog: DatabaseLocationsCatalog; - let ingestionModel: IngestionModel; beforeEach(async () => { + const knex = Knex({ + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + }); + knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => { + resource.run('PRAGMA foreign_keys = ON', () => {}); + }); await knex.migrate.latest({ directory: path.resolve(__dirname, '../database/migrations'), loadExtensions: ['.ts'], }); - db = new CommonDatabase(knex, getVoidLogger()); - ingestionModel = new MockIngestionModel(); - catalog = new DatabaseLocationsCatalog(db, ingestionModel); + const db = new CommonDatabase(knex, getVoidLogger()); + catalog = new DatabaseLocationsCatalog(db); }); - it('resolves to location with id', async () => { - return expect( - catalog.addLocation({ type: 'valid_type', target: 'valid_target' }), - ).resolves.toEqual({ - id: expect.anything(), + it('can add a location', async () => { + const location = { + id: 'dd12620d-0436-422f-93bd-929aa0788123', type: 'valid_type', target: 'valid_target', - }); - }); - it('rejects for invalid type', async () => { - const type = 'invalid_type'; - return expect( - catalog.addLocation({ type, target: 'valid_target' }), - ).rejects.toThrow(/Unknown location type/); - }); - it('rejects for unreadable target ', async () => { - const target = 'invalid_target'; - return expect( - catalog.addLocation({ type: 'valid_type', target }), - ).rejects.toThrow( - `Can't read location at ${target} with error: Something is broken`, - ); + }; + await expect(catalog.addLocation(location)).resolves.toEqual(location); + await expect( + catalog.location('dd12620d-0436-422f-93bd-929aa0788123'), + ).resolves.toEqual(expect.objectContaining({ data: location })); + await expect(catalog.locations()).resolves.toEqual([ + expect.objectContaining({ data: location }), + ]); }); }); diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index ae910e0b9b..2a5208c95a 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -16,38 +16,12 @@ import type { Database } from '../database'; import { DatabaseLocationUpdateLogEvent } from '../database/types'; -import { IngestionModel } from '../ingestion/types'; -import { - AddLocation, - Location, - LocationResponse, - LocationsCatalog, -} from './types'; +import { Location, LocationResponse, LocationsCatalog } from './types'; export class DatabaseLocationsCatalog implements LocationsCatalog { - constructor( - private readonly database: Database, - private readonly ingestionModel: IngestionModel, - ) {} - - async addLocation(location: AddLocation): Promise { - const outputs = await this.ingestionModel.readLocation( - location.type, - location.target, - ); - if (!outputs) { - throw new Error( - `Unknown location type ${location.type} ${location.target}`, - ); - } - outputs.forEach(output => { - if (output.type === 'error') { - throw new Error( - `Can't read location at ${location.target}, ${output.error}`, - ); - } - }); + constructor(private readonly database: Database) {} + async addLocation(location: Location): Promise { const added = await this.database.addLocation(location); return added; } diff --git a/plugins/catalog-backend/src/catalog/index.ts b/plugins/catalog-backend/src/catalog/index.ts index 6768268f34..dc0bb2e84a 100644 --- a/plugins/catalog-backend/src/catalog/index.ts +++ b/plugins/catalog-backend/src/catalog/index.ts @@ -17,10 +17,9 @@ export { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; export { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; export { StaticEntitiesCatalog } from './StaticEntitiesCatalog'; -export { addLocationSchema } from './types'; export type { - AddLocation, EntitiesCatalog, Location, LocationsCatalog, + LocationSpec, } from './types'; diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 2ef5fd9e56..a8c3c7a15e 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -15,7 +15,6 @@ */ import { Entity } from '@backstage/catalog-model'; -import * as yup from 'yup'; import type { EntityFilters } from '../database'; // @@ -52,31 +51,22 @@ export type LocationUpdateLogEvent = { message?: string; }; -export type Location = { - id: string; +export type LocationSpec = { type: string; target: string; }; +export type Location = { + id: string; +} & LocationSpec; + export type LocationResponse = { data: Location; currentStatus: LocationUpdateStatus; }; -export type AddLocation = { - type: string; - target: string; -}; - -export const addLocationSchema: yup.Schema = yup - .object({ - type: yup.string().required(), - target: yup.string().required(), - }) - .noUnknown(); - export type LocationsCatalog = { - addLocation(location: AddLocation): Promise; + addLocation(location: Location): Promise; removeLocation(id: string): Promise; locations(): Promise; location(id: string): Promise; diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts index 3b783a7691..8f62969bd1 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts @@ -22,13 +22,12 @@ import { import type { Entity } from '@backstage/catalog-model'; import Knex from 'knex'; import path from 'path'; +import { Location } from '../catalog'; import { CommonDatabase } from './CommonDatabase'; import { DatabaseLocationUpdateLogStatus } from './types'; import type { - AddDatabaseLocation, DbEntityRequest, DbEntityResponse, - DbLocationsRow, DbLocationsRowWithStatus, } from './types'; @@ -87,9 +86,13 @@ describe('CommonDatabase', () => { it('manages locations', async () => { const db = new CommonDatabase(knex, getVoidLogger()); - const input: AddDatabaseLocation = { type: 'a', target: 'b' }; + const input: Location = { + id: 'dd12620d-0436-422f-93bd-929aa0788123', + type: 'a', + target: 'b', + }; const output: DbLocationsRowWithStatus = { - id: expect.anything(), + id: 'dd12620d-0436-422f-93bd-929aa0788123', type: 'a', target: 'b', message: null, @@ -112,22 +115,6 @@ describe('CommonDatabase', () => { ); }); - it('instead of adding second location with the same target, returns existing one', async () => { - // Prepare - const catalog = new CommonDatabase(knex, getVoidLogger()); - const input: AddDatabaseLocation = { type: 'a', target: 'b' }; - const output1: DbLocationsRow = await catalog.addLocation(input); - - // Try to insert the same location - const output2: DbLocationsRow = await catalog.addLocation(input); - const locations = await catalog.locations(); - - // Output is the same - expect(output2).toEqual(output1); - // Locations contain only one record - expect(locations[0]).toMatchObject(output1); - }); - describe('addEntity', () => { it('happy path: adds entity to empty database', async () => { const catalog = new CommonDatabase(knex, getVoidLogger()); @@ -160,27 +147,33 @@ describe('CommonDatabase', () => { describe('locationHistory', () => { it('outputs the history correctly', async () => { const catalog = new CommonDatabase(knex, getVoidLogger()); - const location: AddDatabaseLocation = { type: 'a', target: 'b' }; - const { id: locationId } = await catalog.addLocation(location); + const location: Location = { + id: 'dd12620d-0436-422f-93bd-929aa0788123', + type: 'a', + target: 'b', + }; + await catalog.addLocation(location); await catalog.addLocationUpdateLogEvent( - locationId, + 'dd12620d-0436-422f-93bd-929aa0788123', DatabaseLocationUpdateLogStatus.SUCCESS, ); await catalog.addLocationUpdateLogEvent( - locationId, + 'dd12620d-0436-422f-93bd-929aa0788123', DatabaseLocationUpdateLogStatus.FAIL, undefined, 'Something went wrong', ); - const result = await catalog.locationHistory(locationId); + const result = await catalog.locationHistory( + 'dd12620d-0436-422f-93bd-929aa0788123', + ); expect(result).toEqual([ { created_at: expect.anything(), entity_name: null, id: expect.anything(), - location_id: locationId, + location_id: 'dd12620d-0436-422f-93bd-929aa0788123', message: null, status: DatabaseLocationUpdateLogStatus.SUCCESS, }, @@ -188,7 +181,7 @@ describe('CommonDatabase', () => { created_at: expect.anything(), entity_name: null, id: expect.anything(), - location_id: locationId, + location_id: 'dd12620d-0436-422f-93bd-929aa0788123', message: 'Something went wrong', status: DatabaseLocationUpdateLogStatus.FAIL, }, diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index 2dac08b9bc..084f6ea662 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -26,7 +26,6 @@ import { v4 as uuidv4 } from 'uuid'; import type { Logger } from 'winston'; import { buildEntitySearch } from './search'; import type { - AddDatabaseLocation, Database, DatabaseLocationUpdateLogEvent, DatabaseLocationUpdateLogStatus, @@ -38,6 +37,7 @@ import type { DbLocationsRowWithStatus, EntityFilters, } from './types'; +import { Location } from '../catalog'; function getStrippedMetadata(metadata: EntityMeta): EntityMeta { const output = lodash.cloneDeep(metadata); @@ -336,25 +336,15 @@ export class CommonDatabase implements Database { } } - async addLocation(location: AddDatabaseLocation): Promise { + async addLocation(location: Location): Promise { return await this.database.transaction(async tx => { - const existingLocation = await tx('locations') - .where({ target: location.target }) - .select(); - - if (existingLocation?.[0]) { - return existingLocation[0]; - } - - const id = uuidv4(); - const { type, target } = location; - await tx('locations').insert({ - id, - type, - target, - }); - - return (await tx('locations').where({ id }).select())![0]; + const row: DbLocationsRow = { + id: location.id, + type: location.type, + target: location.target, + }; + await tx('locations').insert(row); + return row; }); } diff --git a/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts b/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts index 2ff06c4046..5f136670f4 100644 --- a/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts +++ b/plugins/catalog-backend/src/database/migrations/20200511113813_init.ts @@ -26,7 +26,11 @@ export async function up(knex: Knex): Promise { table.comment( 'Registered locations that shall be contiuously scanned for catalog item updates', ); - table.uuid('id').primary().comment('Auto-generated ID of the location'); + table + .uuid('id') + .primary() + .notNullable() + .comment('Auto-generated ID of the location'); table.string('type').notNullable().comment('The type of location'); table .string('target') diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index f69a7353c7..4bcc66af1c 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -15,7 +15,7 @@ */ import type { Entity } from '@backstage/catalog-model'; -import * as yup from 'yup'; +import { Location } from '../catalog'; export type DbEntitiesRow = { id: string; @@ -58,18 +58,6 @@ export type DbLocationsRowWithStatus = DbLocationsRow & { message: string | null; }; -export type AddDatabaseLocation = { - type: string; - target: string; -}; - -export const addDatabaseLocationSchema: yup.Schema = yup - .object({ - type: yup.string().required(), - target: yup.string().required(), - }) - .noUnknown(); - export enum DatabaseLocationUpdateLogStatus { FAIL = 'fail', SUCCESS = 'success', @@ -145,7 +133,7 @@ export type Database = { removeEntity(tx: unknown, uid: string): Promise; - addLocation(location: AddDatabaseLocation): Promise; + addLocation(location: Location): Promise; removeLocation(id: string): Promise; diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts new file mode 100644 index 0000000000..0314556e27 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -0,0 +1,119 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { v4 as uuidv4 } from 'uuid'; +import { + EntitiesCatalog, + Location, + LocationsCatalog, + LocationSpec, +} from '../catalog'; +import { IngestionModel } from '../ingestion'; +import { AddLocationResult, HigherOrderOperation } from './types'; + +const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; + +/** + * Placeholder for operations that span several catalogs and/or stretches out + * in time. + * + * TODO(freben): Find a better home for these, possibly refactoring to use the + * database more directly. + */ +export class HigherOrderOperations implements HigherOrderOperation { + private readonly entitiesCatalog: EntitiesCatalog; + private readonly locationsCatalog: LocationsCatalog; + private readonly ingestionModel: IngestionModel; + + constructor( + entitiesCatalog: EntitiesCatalog, + locationsCatalog: LocationsCatalog, + ingestionModel: IngestionModel, + ) { + this.entitiesCatalog = entitiesCatalog; + this.locationsCatalog = locationsCatalog; + this.ingestionModel = ingestionModel; + } + + /** + * Adds a single location to the catalog. + * + * The location is inspected and fetched, and all of the resulting data is + * validated. If everything goes well, the location and entities are stored + * in the catalog. + * + * If the location already existed, the old location is returned instead and + * the catalog is left unchanged. + * + * @param spec The location to add + */ + async addLocation(spec: LocationSpec): Promise { + // Attempt to find a previous location matching the spec + const previousLocations = await this.locationsCatalog.locations(); + const previousLocation = previousLocations.find( + l => spec.type === l.data.type && spec.target === l.data.target, + ); + const location: Location = previousLocation + ? previousLocation.data + : { + id: uuidv4(), + type: spec.type, + target: spec.target, + }; + + // Read the location fully, bailing on any errors + const readerOutput = await this.ingestionModel.readLocation( + location.type, + location.target, + ); + const inputEntities: Entity[] = []; + for (const entry of readerOutput) { + if (entry.type === 'error') { + throw new InputError( + `Failed to read location ${location.type} ${location.target}, ${entry.error}`, + ); + } else { + // Append the location reference annotation + entry.data.metadata.annotations = { + ...entry.data.metadata.annotations, + [LOCATION_ANNOTATION]: location.id, + }; + inputEntities.push(entry.data); + } + } + + // TODO(freben): At this point, we could detect orphaned entities, by way + // of having a LOCATION_ANNOTATION pointing to the location but not being + // in the entities list. But we aren't sure what to do about those yet. + + // Write + if (!previousLocation) { + await this.locationsCatalog.addLocation(location); + } + const outputEntities: Entity[] = []; + for (const entity of inputEntities) { + const out = await this.entitiesCatalog.addOrUpdateEntity( + entity, + location.id, + ); + outputEntities.push(out); + } + + return { location, entities: outputEntities }; + } +} diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index b6aceaecdd..93af856656 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -15,6 +15,7 @@ */ export * from './descriptor'; +export { HigherOrderOperations } from './HigherOrderOperations'; export { IngestionModels } from './IngestionModels'; export * from './source'; export type { IngestionModel } from './types'; diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index 8878c2af5b..45dd0d9f74 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -14,8 +14,19 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; +import { Location, LocationSpec } from '../catalog'; import { ReaderOutput } from './descriptor/parsers/types'; +export type AddLocationResult = { + location: Location; + entities: Entity[]; +}; + export type IngestionModel = { readLocation(type: string, target: string): Promise; }; + +export type HigherOrderOperation = { + addLocation(spec: LocationSpec): Promise; +}; diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 4092ed1395..e03cb3dc98 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -18,42 +18,52 @@ import { getVoidLogger, NotFoundError } from '@backstage/backend-common'; import type { Entity } from '@backstage/catalog-model'; import express from 'express'; import request from 'supertest'; -import { EntitiesCatalog, Location, LocationsCatalog } from '../catalog'; +import { EntitiesCatalog, LocationsCatalog, LocationSpec } from '../catalog'; +import { LocationResponse } from '../catalog/types'; +import { HigherOrderOperation } from '../ingestion/types'; import { createRouter } from './router'; -class MockEntitiesCatalog implements EntitiesCatalog { - entities = jest.fn(); - entityByUid = jest.fn(); - entityByName = jest.fn(); - addEntity = jest.fn(); - addOrUpdateEntity = jest.fn(); - removeEntityByUid = jest.fn(); -} - -class MockLocationsCatalog implements LocationsCatalog { - addLocation = jest.fn(); - removeLocation = jest.fn(); - locations = jest.fn(); - location = jest.fn(); - locationHistory = jest.fn(); -} - describe('createRouter', () => { + let entitiesCatalog: jest.Mocked; + let locationsCatalog: jest.Mocked; + let higherOrderOperation: jest.Mocked; + let app: express.Express; + + beforeEach(async () => { + entitiesCatalog = { + entities: jest.fn(), + entityByUid: jest.fn(), + entityByName: jest.fn(), + addOrUpdateEntity: jest.fn(), + removeEntityByUid: jest.fn(), + }; + locationsCatalog = { + addLocation: jest.fn(), + removeLocation: jest.fn(), + locations: jest.fn(), + location: jest.fn(), + locationHistory: jest.fn(), + }; + higherOrderOperation = { + addLocation: jest.fn(), + }; + const router = await createRouter({ + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + logger: getVoidLogger(), + }); + app = express().use(router); + }); + describe('GET /entities', () => { it('happy path: lists entities', async () => { const entities: Entity[] = [ { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, ]; - const catalog = new MockEntitiesCatalog(); - catalog.entities.mockResolvedValueOnce(entities); + entitiesCatalog.entities.mockResolvedValueOnce(entities); - const router = await createRouter({ - entitiesCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); const response = await request(app).get('/entities'); expect(response.status).toEqual(200); @@ -61,18 +71,10 @@ describe('createRouter', () => { }); it('parses single and multiple request parameters and passes them down', async () => { - const catalog = new MockEntitiesCatalog(); - - const router = await createRouter({ - entitiesCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); const response = await request(app).get('/entities?a=1&a=&a=3&b=4&c='); expect(response.status).toEqual(200); - expect(catalog.entities).toHaveBeenCalledWith([ + expect(entitiesCatalog.entities).toHaveBeenCalledWith([ { key: 'a', values: ['1', null, '3'] }, { key: 'b', values: ['4'] }, { key: 'c', values: [null] }, @@ -89,15 +91,8 @@ describe('createRouter', () => { name: 'c', }, }; - const catalog = new MockEntitiesCatalog(); - catalog.entityByUid.mockResolvedValue(entity); + entitiesCatalog.entityByUid.mockResolvedValue(entity); - const router = await createRouter({ - entitiesCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); const response = await request(app).get('/entities/by-uid/zzz'); expect(response.status).toEqual(200); @@ -105,15 +100,7 @@ describe('createRouter', () => { }); it('responds with a 404 for missing entities', async () => { - const catalog = new MockEntitiesCatalog(); - catalog.entityByUid.mockResolvedValue(undefined); - - const router = await createRouter({ - entitiesCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); + entitiesCatalog.entityByUid.mockResolvedValue(undefined); const response = await request(app).get('/entities/by-uid/zzz'); expect(response.status).toEqual(404); @@ -131,15 +118,8 @@ describe('createRouter', () => { namespace: 'd', }, }; - const catalog = new MockEntitiesCatalog(); - catalog.entityByName.mockResolvedValue(entity); + entitiesCatalog.entityByName.mockResolvedValue(entity); - const router = await createRouter({ - entitiesCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); const response = await request(app).get('/entities/by-name/b/d/c'); expect(response.status).toEqual(200); @@ -147,15 +127,8 @@ describe('createRouter', () => { }); it('responds with a 404 for missing entities', async () => { - const catalog = new MockEntitiesCatalog(); - catalog.entityByName.mockResolvedValue(undefined); + entitiesCatalog.entityByName.mockResolvedValue(undefined); - const router = await createRouter({ - entitiesCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); const response = await request(app).get('/entities/by-name//b/d/c'); expect(response.status).toEqual(404); @@ -165,13 +138,6 @@ describe('createRouter', () => { describe('POST /entities', () => { it('requires a body', async () => { - const catalog = new MockEntitiesCatalog(); - const router = await createRouter({ - entitiesCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); const response = await request(app) .post('/entities') .set('Content-Type', 'application/json') @@ -179,7 +145,7 @@ describe('createRouter', () => { expect(response.status).toEqual(400); expect(response.text).toMatch(/body/); - expect(catalog.addOrUpdateEntity).not.toHaveBeenCalled(); + expect(entitiesCatalog.addOrUpdateEntity).not.toHaveBeenCalled(); }); it('passes the body down', async () => { @@ -192,15 +158,8 @@ describe('createRouter', () => { }, }; - const catalog = new MockEntitiesCatalog(); - catalog.addOrUpdateEntity.mockResolvedValue(entity); + entitiesCatalog.addOrUpdateEntity.mockResolvedValue(entity); - const router = await createRouter({ - entitiesCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); const response = await request(app) .post('/entities') .send(entity) @@ -208,58 +167,46 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(response.body).toEqual(entity); - expect(catalog.addOrUpdateEntity).toHaveBeenCalledTimes(1); - expect(catalog.addOrUpdateEntity).toHaveBeenNthCalledWith(1, entity); + expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenNthCalledWith( + 1, + entity, + ); }); }); describe('DELETE /entities/by-uid/:uid', () => { it('can remove', async () => { - const catalog = new MockEntitiesCatalog(); - catalog.removeEntityByUid.mockResolvedValue(undefined); + entitiesCatalog.removeEntityByUid.mockResolvedValue(undefined); - const router = await createRouter({ - entitiesCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); const response = await request(app).delete('/entities/by-uid/apa'); expect(response.status).toEqual(204); - expect(catalog.removeEntityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); }); it('responds with a 404 for missing entities', async () => { - const catalog = new MockEntitiesCatalog(); - catalog.removeEntityByUid.mockRejectedValue(new NotFoundError('nope')); + entitiesCatalog.removeEntityByUid.mockRejectedValue( + new NotFoundError('nope'), + ); - const router = await createRouter({ - entitiesCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); const response = await request(app).delete('/entities/by-uid/apa'); expect(response.status).toEqual(404); - expect(catalog.removeEntityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); }); }); describe('GET /locations', () => { it('happy path: lists locations', async () => { - const locations: Location[] = [{ id: 'a', type: 'b', target: 'c' }]; + const locations: LocationResponse[] = [ + { + currentStatus: { timestamp: '', status: '', message: '' }, + data: { id: 'a', type: 'b', target: 'c' }, + }, + ]; + locationsCatalog.locations.mockResolvedValueOnce(locations); - const catalog = new MockLocationsCatalog(); - catalog.locations.mockResolvedValueOnce(locations); - - const router = await createRouter({ - locationsCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); const response = await request(app).get('/locations'); expect(response.status).toEqual(200); @@ -269,22 +216,33 @@ describe('createRouter', () => { describe('POST /locations', () => { it('rejects malformed locations', async () => { - const location = ({ - id: 'a', + const spec = ({ typez: 'b', target: 'c', - } as unknown) as Location; + } as unknown) as LocationSpec; - const catalog = new MockLocationsCatalog(); - const router = await createRouter({ - locationsCatalog: catalog, - logger: getVoidLogger(), - }); - - const app = express().use(router); - const response = await request(app).post('/locations').send(location); + const response = await request(app).post('/locations').send(spec); expect(response.status).toEqual(400); + expect(higherOrderOperation.addLocation).not.toHaveBeenCalled(); + }); + + it('passes the body down', async () => { + const spec: LocationSpec = { + type: 'b', + target: 'c', + }; + + higherOrderOperation.addLocation.mockResolvedValue({ + location: { id: 'a', ...spec }, + entities: [], + }); + + const response = await request(app).post('/locations').send(spec); + + expect(response.status).toEqual(201); + expect(higherOrderOperation.addLocation).toHaveBeenCalledTimes(1); + expect(higherOrderOperation.addLocation).toHaveBeenCalledWith(spec); }); }); }); diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index f5adbd84ca..95ea068f58 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -19,24 +19,30 @@ import { Entity } from '@backstage/catalog-model'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; -import { - addLocationSchema, - EntitiesCatalog, - LocationsCatalog, -} from '../catalog'; +import * as yup from 'yup'; +import { EntitiesCatalog, LocationsCatalog, LocationSpec } from '../catalog'; import { EntityFilters } from '../database'; +import { HigherOrderOperation } from '../ingestion/types'; import { requireRequestBody, validateRequestBody } from './util'; export interface RouterOptions { entitiesCatalog?: EntitiesCatalog; locationsCatalog?: LocationsCatalog; + higherOrderOperation?: HigherOrderOperation; logger: Logger; } +const addLocationSchema = yup + .object({ + type: yup.string().required(), + target: yup.string().required(), + }) + .noUnknown(); + export async function createRouter( options: RouterOptions, ): Promise { - const { entitiesCatalog, locationsCatalog } = options; + const { entitiesCatalog, locationsCatalog, higherOrderOperation } = options; const router = Router(); router.use(express.json()); @@ -84,13 +90,16 @@ export async function createRouter( }); } + if (higherOrderOperation) { + router.post('/locations', async (req, res) => { + const input = await validateRequestBody(req, addLocationSchema); + const output = await higherOrderOperation.addLocation(input); + res.status(201).send(output); + }); + } + if (locationsCatalog) { router - .post('/locations', async (req, res) => { - const input = await validateRequestBody(req, addLocationSchema); - const output = await locationsCatalog.addLocation(input); - res.status(201).send(output); - }) .get('/locations', async (_req, res) => { const output = await locationsCatalog.locations(); res.status(200).send(output); From 9db24c79b80625626df5d09c93f12b083462b451 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2020 09:46:12 +0200 Subject: [PATCH 77/97] build(deps): bump ts-jest from 26.0.0 to 26.1.0 (#1099) Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 26.0.0 to 26.1.0. - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v26.0.0...v26.1.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index 74fba61f49..b322cb1da4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18024,9 +18024,9 @@ ts-invariant@^0.4.0: tslib "^1.9.3" ts-jest@^26.0.0: - version "26.0.0" - resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-26.0.0.tgz#957b802978249aaf74180b9dcb17b4fd787ad6f3" - integrity sha512-eBpWH65mGgzobuw7UZy+uPP9lwu+tPp60o324ASRX4Ijg8UC5dl2zcge4kkmqr2Zeuk9FwIjvCTOPuNMEyGWWw== + version "26.1.0" + resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.0.tgz#e9070fc97b3ea5557a48b67c631c74eb35e15417" + integrity sha512-JbhQdyDMYN5nfKXaAwCIyaWLGwevcT2/dbqRPsQeh6NZPUuXjZQZEfeLb75tz0ubCIgEELNm6xAzTe5NXs5Y4Q== dependencies: bs-logger "0.x" buffer-from "1.x" @@ -19211,7 +19211,7 @@ yaml@^1.7.2: dependencies: "@babel/runtime" "^7.8.7" -yargs-parser@18.x: +yargs-parser@18.x, yargs-parser@^18.1.1: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== @@ -19242,14 +19242,6 @@ yargs-parser@^15.0.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.1: - version "18.1.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.1.tgz#bf7407b915427fc760fcbbccc6c82b4f0ffcbd37" - integrity sha512-KRHEsOM16IX7XuLnMOqImcPNbLVXMNHYAoFc3BKR8Ortl5gzDbtXvvEoGx9imk5E+X1VeNKNlcHr8B8vi+7ipA== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs@^13.3.2: version "13.3.2" resolved "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" From 7d4c044d1a58923916e0d296e6ed7656d00da689 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 2 Jun 2020 10:04:20 +0200 Subject: [PATCH 78/97] Rename test to clarify what it is doing --- .../src/apis/implementations/auth/github/GithubAuth.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts index 3d3da266fc..7c8e6ce9f0 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts @@ -16,13 +16,11 @@ import GithubAuth from './GithubAuth'; -const theFuture = new Date(Date.now() + 3600000); - describe('GithubAuth', () => { - it('should get refreshed access token', async () => { + it('should get access token', async () => { const getSession = jest .fn() - .mockResolvedValue({ accessToken: 'access-token', expiresAt: theFuture }); + .mockResolvedValue({ accessToken: 'access-token' }); const githubAuth = new GithubAuth({ getSession } as any); expect(await githubAuth.getAccessToken()).toBe('access-token'); From 8d0e750bc2ab32676214645aeb8c5699b7dd66a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 2 Jun 2020 10:49:23 +0200 Subject: [PATCH 79/97] Address comments and add tests --- packages/catalog-model/src/index.ts | 1 + packages/catalog-model/src/location/index.ts | 18 +++ packages/catalog-model/src/location/types.ts | 24 +++ .../catalog-model/src/location/validation.ts | 33 ++++ .../catalog/DatabaseEntitiesCatalog.test.ts | 6 +- .../src/catalog/DatabaseLocationsCatalog.ts | 3 +- plugins/catalog-backend/src/catalog/index.ts | 7 +- plugins/catalog-backend/src/catalog/types.ts | 11 +- .../src/database/CommonDatabase.test.ts | 3 +- .../src/database/CommonDatabase.ts | 3 +- plugins/catalog-backend/src/database/types.ts | 3 +- .../ingestion/HigherOrderOperations.test.ts | 143 ++++++++++++++++++ .../src/ingestion/HigherOrderOperations.ts | 9 +- .../catalog-backend/src/ingestion/types.ts | 5 +- .../src/service/router.test.ts | 10 +- plugins/catalog-backend/src/service/router.ts | 15 +- 16 files changed, 246 insertions(+), 48 deletions(-) create mode 100644 packages/catalog-model/src/location/index.ts create mode 100644 packages/catalog-model/src/location/types.ts create mode 100644 packages/catalog-model/src/location/validation.ts create mode 100644 plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index fb51461053..f149b8c9b4 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -17,5 +17,6 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; +export * from './location'; export type { EntityPolicy } from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts new file mode 100644 index 0000000000..60465bff9b --- /dev/null +++ b/packages/catalog-model/src/location/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export type { Location, LocationSpec } from './types'; +export { locationSchema, locationSpecSchema } from './validation'; diff --git a/packages/catalog-model/src/location/types.ts b/packages/catalog-model/src/location/types.ts new file mode 100644 index 0000000000..50e6e82a54 --- /dev/null +++ b/packages/catalog-model/src/location/types.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +export type LocationSpec = { + type: string; + target: string; +}; + +export type Location = { + id: string; +} & LocationSpec; diff --git a/packages/catalog-model/src/location/validation.ts b/packages/catalog-model/src/location/validation.ts new file mode 100644 index 0000000000..5fad47bdd0 --- /dev/null +++ b/packages/catalog-model/src/location/validation.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as yup from 'yup'; +import { LocationSpec, Location } from './types'; + +export const locationSpecSchema = yup + .object({ + type: yup.string().required(), + target: yup.string().required(), + }) + .noUnknown(); + +export const locationSchema = yup + .object({ + id: yup.string().required(), + type: yup.string().required(), + target: yup.string().required(), + }) + .noUnknown(); diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts index b185075002..eb9cb2439c 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -21,7 +21,7 @@ import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; describe('DatabaseEntitiesCatalog', () => { let db: jest.Mocked; - beforeEach(() => { + beforeAll(() => { db = { transaction: jest.fn(), addEntity: jest.fn(), @@ -36,6 +36,10 @@ describe('DatabaseEntitiesCatalog', () => { locationHistory: jest.fn(), addLocationUpdateLogEvent: jest.fn(), }; + }); + + beforeEach(() => { + jest.resetAllMocks(); db.transaction.mockImplementation(async f => f('tx')); }); diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index 2a5208c95a..066fab8464 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -14,9 +14,10 @@ * limitations under the License. */ +import { Location } from '@backstage/catalog-model'; import type { Database } from '../database'; import { DatabaseLocationUpdateLogEvent } from '../database/types'; -import { Location, LocationResponse, LocationsCatalog } from './types'; +import { LocationResponse, LocationsCatalog } from './types'; export class DatabaseLocationsCatalog implements LocationsCatalog { constructor(private readonly database: Database) {} diff --git a/plugins/catalog-backend/src/catalog/index.ts b/plugins/catalog-backend/src/catalog/index.ts index dc0bb2e84a..308078b1fc 100644 --- a/plugins/catalog-backend/src/catalog/index.ts +++ b/plugins/catalog-backend/src/catalog/index.ts @@ -17,9 +17,4 @@ export { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; export { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; export { StaticEntitiesCatalog } from './StaticEntitiesCatalog'; -export type { - EntitiesCatalog, - Location, - LocationsCatalog, - LocationSpec, -} from './types'; +export type { EntitiesCatalog, LocationsCatalog } from './types'; diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index a8c3c7a15e..0499b8a409 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { Entity, Location } from '@backstage/catalog-model'; import type { EntityFilters } from '../database'; // @@ -51,15 +51,6 @@ export type LocationUpdateLogEvent = { message?: string; }; -export type LocationSpec = { - type: string; - target: string; -}; - -export type Location = { - id: string; -} & LocationSpec; - export type LocationResponse = { data: Location; currentStatus: LocationUpdateStatus; diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts index 8f62969bd1..f1723e3733 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts @@ -19,10 +19,9 @@ import { getVoidLogger, NotFoundError, } from '@backstage/backend-common'; -import type { Entity } from '@backstage/catalog-model'; +import type { Entity, Location } from '@backstage/catalog-model'; import Knex from 'knex'; import path from 'path'; -import { Location } from '../catalog'; import { CommonDatabase } from './CommonDatabase'; import { DatabaseLocationUpdateLogStatus } from './types'; import type { diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index 084f6ea662..dd965c89ef 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -19,7 +19,7 @@ import { InputError, NotFoundError, } from '@backstage/backend-common'; -import type { Entity, EntityMeta } from '@backstage/catalog-model'; +import type { Entity, EntityMeta, Location } from '@backstage/catalog-model'; import Knex from 'knex'; import lodash from 'lodash'; import { v4 as uuidv4 } from 'uuid'; @@ -37,7 +37,6 @@ import type { DbLocationsRowWithStatus, EntityFilters, } from './types'; -import { Location } from '../catalog'; function getStrippedMetadata(metadata: EntityMeta): EntityMeta { const output = lodash.cloneDeep(metadata); diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 4bcc66af1c..17da373ae9 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import type { Entity } from '@backstage/catalog-model'; -import { Location } from '../catalog'; +import type { Entity, Location } from '@backstage/catalog-model'; export type DbEntitiesRow = { id: string; diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts new file mode 100644 index 0000000000..7f1e890fbf --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts @@ -0,0 +1,143 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntitiesCatalog, LocationsCatalog } from '../catalog'; +import { IngestionModel } from './types'; +import { HigherOrderOperations } from './HigherOrderOperations'; +import { Entity } from '@backstage/catalog-model'; + +describe('HigherOrderOperations', () => { + let entitiesCatalog: jest.Mocked; + let locationsCatalog: jest.Mocked; + let ingestionModel: jest.Mocked; + let higherOrderOperation: HigherOrderOperations; + + beforeAll(() => { + entitiesCatalog = { + entities: jest.fn(), + entityByUid: jest.fn(), + entityByName: jest.fn(), + addOrUpdateEntity: jest.fn(), + removeEntityByUid: jest.fn(), + }; + locationsCatalog = { + addLocation: jest.fn(), + removeLocation: jest.fn(), + locations: jest.fn(), + location: jest.fn(), + locationHistory: jest.fn(), + }; + ingestionModel = { + readLocation: jest.fn(), + }; + higherOrderOperation = new HigherOrderOperations( + entitiesCatalog, + locationsCatalog, + ingestionModel, + ); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('addLocation', () => { + it('just inserts the location when there are no entities to read', async () => { + const spec = { + type: 'a', + target: 'b', + }; + locationsCatalog.addLocation.mockImplementation(x => Promise.resolve(x)); + locationsCatalog.locations.mockResolvedValue([]); + ingestionModel.readLocation.mockResolvedValue([]); + + const result = await higherOrderOperation.addLocation(spec); + + expect(result.location).toEqual( + expect.objectContaining({ + id: expect.anything(), + ...spec, + }), + ); + expect(result.entities).toEqual([]); + expect(locationsCatalog.locations).toBeCalledTimes(1); + expect(ingestionModel.readLocation).toBeCalledTimes(1); + expect(ingestionModel.readLocation).toBeCalledWith('a', 'b'); + expect(entitiesCatalog.addOrUpdateEntity).not.toBeCalled(); + expect(locationsCatalog.addLocation).toBeCalledTimes(1); + expect(locationsCatalog.addLocation).toBeCalledWith( + expect.objectContaining({ + id: expect.anything(), + ...spec, + }), + ); + }); + + it('reuses the location if a match already existed', async () => { + const spec = { + type: 'a', + target: 'b', + }; + const location = { + id: 'dd12620d-0436-422f-93bd-929aa0788123', + ...spec, + }; + + locationsCatalog.locations.mockResolvedValue([ + { + currentStatus: { timestamp: '', status: '', message: '' }, + data: location, + }, + ]); + ingestionModel.readLocation.mockResolvedValue([]); + + const result = await higherOrderOperation.addLocation(spec); + + expect(result.location).toEqual(location); + expect(result.entities).toEqual([]); + expect(locationsCatalog.locations).toBeCalledTimes(1); + expect(ingestionModel.readLocation).toBeCalledTimes(1); + expect(ingestionModel.readLocation).toBeCalledWith('a', 'b'); + expect(entitiesCatalog.addOrUpdateEntity).not.toBeCalled(); + expect(locationsCatalog.addLocation).not.toBeCalled(); + }); + + it('rejects the whole operation if any entity could not be read', async () => { + const spec = { + type: 'a', + target: 'b', + }; + const entity: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { name: 'n' }, + }; + + locationsCatalog.locations.mockResolvedValue([]); + ingestionModel.readLocation.mockResolvedValue([ + { type: 'data', data: entity }, + { type: 'error', error: new Error('abcd') }, + ]); + + await expect(higherOrderOperation.addLocation(spec)).rejects.toThrow( + /abcd/, + ); + expect(locationsCatalog.locations).toBeCalledTimes(1); + expect(entitiesCatalog.addOrUpdateEntity).not.toBeCalled(); + expect(locationsCatalog.addLocation).not.toBeCalled(); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index 0314556e27..e28d1e722a 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -15,14 +15,9 @@ */ import { InputError } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, Location, LocationSpec } from '@backstage/catalog-model'; import { v4 as uuidv4 } from 'uuid'; -import { - EntitiesCatalog, - Location, - LocationsCatalog, - LocationSpec, -} from '../catalog'; +import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { IngestionModel } from '../ingestion'; import { AddLocationResult, HigherOrderOperation } from './types'; diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index 45dd0d9f74..018784bd0f 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { Location, LocationSpec } from '../catalog'; -import { ReaderOutput } from './descriptor/parsers/types'; +import type { Entity, Location, LocationSpec } from '@backstage/catalog-model'; +import type { ReaderOutput } from './descriptor/parsers/types'; export type AddLocationResult = { location: Location; diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index e03cb3dc98..d26476bca0 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -15,10 +15,10 @@ */ import { getVoidLogger, NotFoundError } from '@backstage/backend-common'; -import type { Entity } from '@backstage/catalog-model'; +import type { Entity, LocationSpec } from '@backstage/catalog-model'; import express from 'express'; import request from 'supertest'; -import { EntitiesCatalog, LocationsCatalog, LocationSpec } from '../catalog'; +import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { LocationResponse } from '../catalog/types'; import { HigherOrderOperation } from '../ingestion/types'; import { createRouter } from './router'; @@ -29,7 +29,7 @@ describe('createRouter', () => { let higherOrderOperation: jest.Mocked; let app: express.Express; - beforeEach(async () => { + beforeAll(async () => { entitiesCatalog = { entities: jest.fn(), entityByUid: jest.fn(), @@ -56,6 +56,10 @@ describe('createRouter', () => { app = express().use(router); }); + beforeEach(() => { + jest.resetAllMocks(); + }); + describe('GET /entities', () => { it('happy path: lists entities', async () => { const entities: Entity[] = [ diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 95ea068f58..22d1f73730 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -15,12 +15,12 @@ */ import { errorHandler, InputError } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; +import { locationSpecSchema } from '@backstage/catalog-model'; +import type { Entity } from '@backstage/catalog-model'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; -import * as yup from 'yup'; -import { EntitiesCatalog, LocationsCatalog, LocationSpec } from '../catalog'; +import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { EntityFilters } from '../database'; import { HigherOrderOperation } from '../ingestion/types'; import { requireRequestBody, validateRequestBody } from './util'; @@ -32,13 +32,6 @@ export interface RouterOptions { logger: Logger; } -const addLocationSchema = yup - .object({ - type: yup.string().required(), - target: yup.string().required(), - }) - .noUnknown(); - export async function createRouter( options: RouterOptions, ): Promise { @@ -92,7 +85,7 @@ export async function createRouter( if (higherOrderOperation) { router.post('/locations', async (req, res) => { - const input = await validateRequestBody(req, addLocationSchema); + const input = await validateRequestBody(req, locationSpecSchema); const output = await higherOrderOperation.addLocation(input); res.status(201).send(output); }); From 1010f4d6b120a5ea7267dffc3f28b01abaf90756 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 1 Jun 2020 17:10:50 +0200 Subject: [PATCH 80/97] plugins: added initial mock-idp-backend --- plugins/mock-idp-backend/.eslintrc.js | 3 + plugins/mock-idp-backend/README.md | 7 ++ plugins/mock-idp-backend/package.json | 41 ++++++++++ plugins/mock-idp-backend/src/index.ts | 17 ++++ plugins/mock-idp-backend/src/run.ts | 80 +++++++++++++++++++ plugins/mock-idp-backend/src/service/index.ts | 17 ++++ .../src/service/router.test.ts | 36 +++++++++ .../mock-idp-backend/src/service/router.ts | 34 ++++++++ plugins/mock-idp-backend/src/setupTests.ts | 17 ++++ plugins/mock-idp-backend/tsconfig.json | 15 ++++ 10 files changed, 267 insertions(+) create mode 100644 plugins/mock-idp-backend/.eslintrc.js create mode 100644 plugins/mock-idp-backend/README.md create mode 100644 plugins/mock-idp-backend/package.json create mode 100644 plugins/mock-idp-backend/src/index.ts create mode 100644 plugins/mock-idp-backend/src/run.ts create mode 100644 plugins/mock-idp-backend/src/service/index.ts create mode 100644 plugins/mock-idp-backend/src/service/router.test.ts create mode 100644 plugins/mock-idp-backend/src/service/router.ts create mode 100644 plugins/mock-idp-backend/src/setupTests.ts create mode 100644 plugins/mock-idp-backend/tsconfig.json diff --git a/plugins/mock-idp-backend/.eslintrc.js b/plugins/mock-idp-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/mock-idp-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/mock-idp-backend/README.md b/plugins/mock-idp-backend/README.md new file mode 100644 index 0000000000..c1925e326f --- /dev/null +++ b/plugins/mock-idp-backend/README.md @@ -0,0 +1,7 @@ +# Mock IdP Backend + +Mock backend for demonstrating 3rd party identity provider flow with SAML 2.0. + +## Links + +- (The Backstage homepage)[https://backstage.io] diff --git a/plugins/mock-idp-backend/package.json b/plugins/mock-idp-backend/package.json new file mode 100644 index 0000000000..e4ff95e46a --- /dev/null +++ b/plugins/mock-idp-backend/package.json @@ -0,0 +1,41 @@ +{ + "name": "@backstage/plugin-mock-idp-backend", + "version": "0.1.1-alpha.6", + "main": "dist", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "scripts": { + "start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon dist/run.js\"", + "build": "tsc", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.1.1-alpha.6", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.17.1", + "express-promise-router": "^3.0.3", + "fs-extra": "^9.0.0", + "helmet": "^3.22.0", + "morgan": "^1.10.0", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.6", + "@types/supertest": "^2.0.8", + "supertest": "^4.0.2", + "tsc-watch": "^4.2.3" + }, + "files": [ + "dist" + ], + "nodemonConfig": { + "watch": "./dist" + } +} diff --git a/plugins/mock-idp-backend/src/index.ts b/plugins/mock-idp-backend/src/index.ts new file mode 100644 index 0000000000..7612c392a2 --- /dev/null +++ b/plugins/mock-idp-backend/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export * from './service/router'; diff --git a/plugins/mock-idp-backend/src/run.ts b/plugins/mock-idp-backend/src/run.ts new file mode 100644 index 0000000000..ea67d335fd --- /dev/null +++ b/plugins/mock-idp-backend/src/run.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + errorHandler, + getRootLogger, + notFoundHandler, + requestLoggingHandler, +} from '@backstage/backend-common'; +import compression from 'compression'; +import cors from 'cors'; +import express from 'express'; +import helmet from 'helmet'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './service'; + +export type ServerConfig = { + port: number; + logger: Logger; +}; + +function readConfig() { + const port = Number(process.env.PLUGIN_PORT) || 3003; + const logger = getRootLogger().child({ service: 'mock-idp-backend' }); + return { port, logger }; +} + +async function startStandaloneServer(config: ServerConfig): Promise { + const { port, logger } = config; + logger.debug('Creating application...'); + + const app = express(); + + app.use(helmet()); + app.use(cors()); + app.use(compression()); + app.use(express.json()); + app.use(requestLoggingHandler()); + app.use(await createRouter({ logger })); + app.use(notFoundHandler()); + app.use(errorHandler()); + + logger.debug('Starting application server...'); + + process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); + }); + + return await new Promise((resolve, reject) => { + const server = app.listen(port, (err?: Error) => { + if (err) { + reject(err); + return; + } + + logger.info(`Listening on port ${port}`); + resolve(server); + }); + }); +} + +startStandaloneServer(readConfig()).catch(err => { + console.error(err.stack || err); + process.exit(1); +}); diff --git a/plugins/mock-idp-backend/src/service/index.ts b/plugins/mock-idp-backend/src/service/index.ts new file mode 100644 index 0000000000..38fbb697c4 --- /dev/null +++ b/plugins/mock-idp-backend/src/service/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { createRouter } from './router'; diff --git a/plugins/mock-idp-backend/src/service/router.test.ts b/plugins/mock-idp-backend/src/service/router.test.ts new file mode 100644 index 0000000000..192baeebb9 --- /dev/null +++ b/plugins/mock-idp-backend/src/service/router.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { createRouter } from './router'; +import express from 'express'; +import request from 'supertest'; + +async function makeApp() { + const router = await createRouter({ logger: getVoidLogger() }); + const app = express(); + app.use(router); + return app; +} + +describe('router', () => { + it('should echo', async () => { + const app = await makeApp(); + const response = await request(app).get('/echo'); + expect(response.status).toEqual(200); + expect(response.text).toEqual('echo'); + }); +}); diff --git a/plugins/mock-idp-backend/src/service/router.ts b/plugins/mock-idp-backend/src/service/router.ts new file mode 100644 index 0000000000..62bffb050d --- /dev/null +++ b/plugins/mock-idp-backend/src/service/router.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +export interface RouterOptions { + logger: Logger; +} + +export async function createRouter(options: RouterOptions) { + const { logger } = options; + const router = Router(); + + router.get('/echo', (_req, res) => { + logger.info('sending echo'); + res.send('echo'); + }); + + return router; +} diff --git a/plugins/mock-idp-backend/src/setupTests.ts b/plugins/mock-idp-backend/src/setupTests.ts new file mode 100644 index 0000000000..ba33cf996b --- /dev/null +++ b/plugins/mock-idp-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export {}; diff --git a/plugins/mock-idp-backend/tsconfig.json b/plugins/mock-idp-backend/tsconfig.json new file mode 100644 index 0000000000..015a967f76 --- /dev/null +++ b/plugins/mock-idp-backend/tsconfig.json @@ -0,0 +1,15 @@ +{ + "include": ["src"], + "compilerOptions": { + "outDir": "dist", + "incremental": true, + "sourceMap": true, + "declaration": true, + "strict": true, + "target": "es2019", + "module": "commonjs", + "esModuleInterop": true, + "lib": ["es2019"], + "types": ["node", "jest"] + } +} From 922c02884693b5083f4fa626a82b5189cd5b1743 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 1 Jun 2020 17:44:41 +0200 Subject: [PATCH 81/97] plugins/mock-idp-backend: generate some dev certs --- plugins/mock-idp-backend/cert.pem | 11 +++++++++++ plugins/mock-idp-backend/gen-certs.sh | 10 ++++++++++ plugins/mock-idp-backend/key.pem | 16 ++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 plugins/mock-idp-backend/cert.pem create mode 100755 plugins/mock-idp-backend/gen-certs.sh create mode 100644 plugins/mock-idp-backend/key.pem diff --git a/plugins/mock-idp-backend/cert.pem b/plugins/mock-idp-backend/cert.pem new file mode 100644 index 0000000000..9fd9f66f97 --- /dev/null +++ b/plugins/mock-idp-backend/cert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBnzCCAQgCCQCIF7n8FCJ0azANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls +b2NhbGhvc3QwHhcNMjAwNjAxMTU0NDM1WhcNMzAwNTMwMTU0NDM1WjAUMRIwEAYD +VQQDDAlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMoHudQ6 +bah8Div++6Ooi+ZchrseVWgXU+q/X6jIY7Gkf/aUMAYtiBAvr/FG5WTn1ePVdHxo +20VkNRkiwFGeL0Z33IrGhkgkwpQZnWGwKpwqEddjed1ABeOksP/RFmzBCHNnxZGu +xu5E1nqWkPLjPRicAoo9V76xPLLlUJoS5ls7AgMBAAEwDQYJKoZIhvcNAQELBQAD +gYEAkG2oNLjFk0FUeM6cqnLvaA9fCml3JUA1nFgMshVXLvE/aOeXKhgy96DAcUw3 +xsdHHKMJpX4n49Odz3wyAtCWFTkz600T7oSrZ7FAU2VORKapBqqUL2HtLGB2gds+ +ndfiDwLV7Yb67cQikkgLU9ZzwYcGL6ovfxuw01f13ZyiAwg= +-----END CERTIFICATE----- diff --git a/plugins/mock-idp-backend/gen-certs.sh b/plugins/mock-idp-backend/gen-certs.sh new file mode 100755 index 0000000000..d33c0f0e75 --- /dev/null +++ b/plugins/mock-idp-backend/gen-certs.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +openssl req \ + -x509 \ + -newkey rsa:1024 \ + -days 3650 \ + -nodes \ + -subj '/CN=localhost' \ + -keyout "key.pem" \ + -out "cert.pem" diff --git a/plugins/mock-idp-backend/key.pem b/plugins/mock-idp-backend/key.pem new file mode 100644 index 0000000000..a90f813106 --- /dev/null +++ b/plugins/mock-idp-backend/key.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMoHudQ6bah8Div+ ++6Ooi+ZchrseVWgXU+q/X6jIY7Gkf/aUMAYtiBAvr/FG5WTn1ePVdHxo20VkNRki +wFGeL0Z33IrGhkgkwpQZnWGwKpwqEddjed1ABeOksP/RFmzBCHNnxZGuxu5E1nqW +kPLjPRicAoo9V76xPLLlUJoS5ls7AgMBAAECgYEAk2hztGA1Zrutc3iGjvEJsrlM +LPeDBYIP3rZny7T62MDYBUg2iitqnbTNwVQjSC7IJ4a1iZjQdJ8bBw4OGP88Kfcv +QQl7PnU7+Sp/z6PMGpcsP/bBoStrqG4Bdv5bM15csLuGlkz/rQV/4Q/9nrCY4qDp +5U7Np/E2TTrjjPINtMECQQD53Q8hl8PkvH5pdqm+GwmGIQu1++WX8kD90ib7hnaL +Vs39pMGFviXmW8LLv0rwFhLNpVCNBpjh4jA8knKFYfexAkEAzv3t4whHUrzdxbBk +TXAg/JTg3KuiYNY64Pq5LzwSYgWNmbcD/FTjy3a7WQd6UzLpcZJ7armtwWaTOkWR +9rZoqwJBAJ/IxAJhgT5nZBehcM9HjwGdZFXObnaKzxECMTesN2bH7hcEI1WZ0bbM +e3e8Lvn1w7SKwUZOL7pT4TD7Hg06JyECQQDCwmpyk/eIAe0pdS7rLfXbsrlg6J2A +QBJmXYKgzwT89fymBW3anoU3jB/7RO30GpNMKWe2o765mqosygjs+fTBAkBdAny5 +Tixg8VNC32dbqlwzcZ8EvrgAoaGZc9HnS3Ay5/OUkMFvyjzNEF2I/brqe2wDPQg0 +nKpWXqNkODXwtYmF +-----END PRIVATE KEY----- From 3573c74fca66964e7a9c8cf4a07f6e9e7ec9b61f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 10:56:15 +0200 Subject: [PATCH 82/97] plugins/mock-idp-backend: add saml-idp + test command --- .../{key.pem => idp-private-key.pem} | 0 .../{cert.pem => idp-public-cert.pem} | 0 plugins/mock-idp-backend/package.json | 2 + yarn.lock | 201 +++++++++++++++++- 4 files changed, 194 insertions(+), 9 deletions(-) rename plugins/mock-idp-backend/{key.pem => idp-private-key.pem} (100%) rename plugins/mock-idp-backend/{cert.pem => idp-public-cert.pem} (100%) diff --git a/plugins/mock-idp-backend/key.pem b/plugins/mock-idp-backend/idp-private-key.pem similarity index 100% rename from plugins/mock-idp-backend/key.pem rename to plugins/mock-idp-backend/idp-private-key.pem diff --git a/plugins/mock-idp-backend/cert.pem b/plugins/mock-idp-backend/idp-public-cert.pem similarity index 100% rename from plugins/mock-idp-backend/cert.pem rename to plugins/mock-idp-backend/idp-public-cert.pem diff --git a/plugins/mock-idp-backend/package.json b/plugins/mock-idp-backend/package.json index e4ff95e46a..451004ca79 100644 --- a/plugins/mock-idp-backend/package.json +++ b/plugins/mock-idp-backend/package.json @@ -7,6 +7,7 @@ "private": true, "scripts": { "start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon dist/run.js\"", + "start:idp": "saml-idp --acsUrl=http://localhost:3003/auth/saml/handler/frame --audience http://localhost:3003", "build": "tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", @@ -23,6 +24,7 @@ "fs-extra": "^9.0.0", "helmet": "^3.22.0", "morgan": "^1.10.0", + "saml-idp": "^1.2.1", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/yarn.lock b/yarn.lock index 760f1421b7..46dca51b6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@auth0/thumbprint@0.0.6": + version "0.0.6" + resolved "https://registry.npmjs.org/@auth0/thumbprint/-/thumbprint-0.0.6.tgz#cab1062c6c04662ce6c592d48157ec4268ae8518" + integrity sha1-yrEGLGwEZizmxZLUgVfsQmiuhRg= + "@babel/code-frame@7.5.5": version "7.5.5" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" @@ -4943,7 +4948,7 @@ async-limiter@~1.0.0: resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== -async@^2.6.1, async@^2.6.2: +async@^2.1.5, async@^2.6.1, async@^2.6.2, async@~2.6.2: version "2.6.3" resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -5453,7 +5458,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== -body-parser@1.19.0: +body-parser@1.19.0, body-parser@~1.19.0: version "1.19.0" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -7249,7 +7254,7 @@ debug@3.1.0, debug@=3.1.0: dependencies: ms "2.0.0" -debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -7839,7 +7844,12 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -ejs@^2.7.4: +ejs@2.6.1: + version "2.6.1" + resolved "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" + integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== + +ejs@^2.5.6, ejs@^2.7.4: version "2.7.4" resolved "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== @@ -8515,6 +8525,20 @@ express-promise-router@^3.0.3: lodash.flattendeep "^4.0.0" methods "^1.0.0" +express-session@^1.17.1: + version "1.17.1" + resolved "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz#36ecbc7034566d38c8509885c044d461c11bf357" + integrity sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q== + dependencies: + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.0.2" + parseurl "~1.3.3" + safe-buffer "5.2.0" + uid-safe "~2.1.5" + express@^4.17.0, express@^4.17.1: version "4.17.1" resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -8566,7 +8590,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.2: +extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -8941,6 +8965,15 @@ flatted@^2.0.0: resolved "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flowstate@^0.4.0: + version "0.4.1" + resolved "https://registry.npmjs.org/flowstate/-/flowstate-0.4.1.tgz#b5fbb8b7fc2d7bdc5b54be46c98309ef736f4ec0" + integrity sha1-tfu4t/wte9xbVL5GyYMJ73NvTsA= + dependencies: + clone "^1.0.2" + uid-safe "^2.1.0" + utils-flatten "^1.0.0" + flush-write-stream@^1.0.0: version "1.1.1" resolved "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" @@ -8997,6 +9030,11 @@ for-own@^1.0.0: dependencies: for-in "^1.0.1" +foreachasync@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" + integrity sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY= + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -9732,7 +9770,7 @@ handle-thing@^2.0.0: resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@^4.4.0, handlebars@^4.7.3: +handlebars@4.7.6, handlebars@^4.4.0, handlebars@^4.7.3: version "4.7.6" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== @@ -9868,6 +9906,14 @@ hastscript@^5.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" +hbs@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/hbs/-/hbs-4.1.1.tgz#8aab17ca6ae70f9aaa225278bed7af31011254b7" + integrity sha512-6QsbB4RwbpL4cb4DNyjEEPF+suwp+3yZqFVlhILEn92ScC0U4cDCR+FDX53jkfKJPhutcqhAvs+rOLZw5sQrDA== + dependencies: + handlebars "4.7.6" + walk "2.3.14" + he@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -13210,6 +13256,11 @@ node-forge@0.9.0: resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== +node-forge@^0.7.0: + version "0.7.6" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" + integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== + node-gyp@^5.0.2: version "5.1.0" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz#8e31260a7af4a2e2f994b0673d4e0b3866156332" @@ -15130,6 +15181,11 @@ ramda@^0.21.0: resolved "https://registry.npmjs.org/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" integrity sha1-oAGr7bP/YQd9T/HVd9RN536NCjU= +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -16368,7 +16424,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@5.2.0, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -16390,6 +16446,51 @@ safe-regex@^1.1.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +saml-idp@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/saml-idp/-/saml-idp-1.2.1.tgz#2df394cd406ce273641115f587062ea60d29ce03" + integrity sha512-C7iXTxryohn8fOWUGyPDCJwyA4eWyh4gJrMAndyRe+idTrj51kPOW1FAAsIBc7afmoJLM00qqcAP/gxalq4r9A== + dependencies: + body-parser "~1.19.0" + chalk "^4.0.0" + debug "~4.1.1" + express "^4.17.1" + express-session "^1.17.1" + extend "^3.0.2" + hbs "^4.1.1" + morgan "^1.10.0" + samlp "github:mcguinness/node-samlp" + xml-formatter "^2.1.0" + xmldom "^0.3.0" + yargs "^15.3.1" + +"saml@github:mcguinness/node-saml": + version "0.12.5" + resolved "https://codeload.github.com/mcguinness/node-saml/tar.gz/ec47b9ab43ad756a5d1fbc82c71e260f7a5cb18a" + dependencies: + async "~2.6.2" + moment "2.24.0" + valid-url "~1.0.9" + xml-crypto "~1.3.0" + xml-encryption "0.11.2" + xml-name-validator "~3.0.0" + xmldom "=0.1.27" + xpath "0.0.27" + +"samlp@github:mcguinness/node-samlp": + version "3.4.1" + resolved "https://codeload.github.com/mcguinness/node-samlp/tar.gz/7bfb7c29be520f249beff6a9e933b63a628e31a1" + dependencies: + "@auth0/thumbprint" "0.0.6" + ejs "2.6.1" + flowstate "^0.4.0" + querystring "^0.2.0" + saml "github:mcguinness/node-saml" + xml-crypto "^1.3.0" + xmldom "github:auth0/xmldom#v0.1.19-auth0_1" + xpath "0.0.27" + xtend "^4.0.1" + sane@^4.0.3: version "4.1.0" resolved "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" @@ -18224,6 +18325,13 @@ uid-number@0.0.6: resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= +uid-safe@^2.1.0, uid-safe@~2.1.5: + version "2.1.5" + resolved "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== + dependencies: + random-bytes "~1.0.0" + uid2@0.0.x: version "0.0.3" resolved "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" @@ -18574,6 +18682,11 @@ utila@^0.4.0, utila@~0.4: resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= +utils-flatten@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/utils-flatten/-/utils-flatten-1.0.0.tgz#01f30d3193be464c40b31755e6740d0db0cef243" + integrity sha1-AfMNMZO+RkxAsxdV5nQNDbDO8kM= + utils-merge@1.0.1, utils-merge@1.x.x: version "1.0.1" resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -18615,7 +18728,7 @@ v8flags@^3.1.3: dependencies: homedir-polyfill "^1.0.1" -valid-url@1.0.9: +valid-url@1.0.9, valid-url@~1.0.9: version "1.0.9" resolved "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= @@ -18722,6 +18835,13 @@ wait-on@4.0.0: request-promise-native "^1.0.8" rxjs "^6.5.4" +walk@2.3.14: + version "2.3.14" + resolved "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz#60ec8631cfd23276ae1e7363ce11d626452e1ef3" + integrity sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg== + dependencies: + foreachasync "^3.0.0" + walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" @@ -19171,16 +19291,79 @@ xdg-basedir@^4.0.0: resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== -xml-name-validator@^3.0.0: +xml-crypto@^1.3.0: + version "1.5.3" + resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.5.3.tgz#a8f500b90f0dfaf0efa3331c345ecb0fff993c34" + integrity sha512-uHkmpUtX15xExe5iimPmakAZN+6CqIvjmaJTy4FwqGzaTjrKRBNeqMh8zGEzVNgW0dk6beFYpyQSgqV/J6C5xA== + dependencies: + xmldom "0.1.27" + xpath "0.0.27" + +xml-crypto@~1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.3.0.tgz#5450e0768c24a854a5cfea6c485d2b73c835d9e1" + integrity sha512-Kx/owhke7oy89NAB8HTkaENc1BaCixQDHD6Wg61VTIOdjBlIRLNs2Ts76MhJz78EPyOMoqUoY4ytShCqbv1XBA== + dependencies: + xmldom "0.1.27" + xpath "0.0.27" + +xml-encryption@0.11.2: + version "0.11.2" + resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-0.11.2.tgz#c217f5509547e34b500b829f2c0bca85cca73a21" + integrity sha512-jVvES7i5ovdO7N+NjgncA326xYKjhqeAnnvIgRnY7ROLCfFqEDLwP0Sxp/30SHG0AXQV1048T5yinOFyvwGFzg== + dependencies: + async "^2.1.5" + ejs "^2.5.6" + node-forge "^0.7.0" + xmldom "~0.1.15" + xpath "0.0.27" + +xml-formatter@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/xml-formatter/-/xml-formatter-2.1.0.tgz#ff438be6e2195e480b7525ecd3b06652ed76f390" + integrity sha512-t55v5mfpohwKvNbfd8A0FZSZI22//hqXqx3AwRx3mjZel0IEoRM2p1bvVnvNPxHqdqZ0sDjUrBqfHJNbIfE8fw== + dependencies: + xml-parser-xo "^3.0.0" + +xml-name-validator@^3.0.0, xml-name-validator@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml-parser-xo@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-3.0.0.tgz#4d46f1962e5100f228b5f73f34c61bb798430195" + integrity sha512-MPPexqXBx48m3OFMQXxo7+RYhG6o6kCGflk4q4oL3uQ0b7d5NDKjHFDwUoozOTPT3WFztT13z3R9Sn0QCTIJcQ== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmldom@0.1.27, xmldom@=0.1.27: + version "0.1.27" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= + +xmldom@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.3.0.tgz#e625457f4300b5df9c2e1ecb776147ece47f3e5a" + integrity sha512-z9s6k3wxE+aZHgXYxSTpGDo7BYOUfJsIRyoZiX6HTjwpwfS2wpQBQKa2fD+ShLyPkqDYo5ud7KitmLZ2Cd6r0g== + +"xmldom@github:auth0/xmldom#v0.1.19-auth0_1": + version "0.1.19" + resolved "https://codeload.github.com/auth0/xmldom/tar.gz/3376bc7beb5551bf68e12b0cc6b0e3669f77d392" + +xmldom@~0.1.15: + version "0.1.31" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" + integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== + +xpath@0.0.27: + version "0.0.27" + resolved "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92" + integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ== + xregexp@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" From 397e670a2e4d9f4a209e972485fbaad57477d494 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 11:18:36 +0200 Subject: [PATCH 83/97] plugins/auth-backend: add script for starting up saml test idp --- plugins/auth-backend/scripts/.gitignore | 1 + .../auth-backend/scripts/start-saml-idp.sh | 17 ++++++ plugins/mock-idp-backend/gen-certs.sh | 10 ---- plugins/mock-idp-backend/idp-private-key.pem | 16 ------ plugins/mock-idp-backend/idp-public-cert.pem | 11 ---- yarn.lock | 57 +++++++++++++++---- 6 files changed, 65 insertions(+), 47 deletions(-) create mode 100644 plugins/auth-backend/scripts/.gitignore create mode 100755 plugins/auth-backend/scripts/start-saml-idp.sh delete mode 100755 plugins/mock-idp-backend/gen-certs.sh delete mode 100644 plugins/mock-idp-backend/idp-private-key.pem delete mode 100644 plugins/mock-idp-backend/idp-public-cert.pem diff --git a/plugins/auth-backend/scripts/.gitignore b/plugins/auth-backend/scripts/.gitignore new file mode 100644 index 0000000000..cfaad76118 --- /dev/null +++ b/plugins/auth-backend/scripts/.gitignore @@ -0,0 +1 @@ +*.pem diff --git a/plugins/auth-backend/scripts/start-saml-idp.sh b/plugins/auth-backend/scripts/start-saml-idp.sh new file mode 100755 index 0000000000..dd80ca6c32 --- /dev/null +++ b/plugins/auth-backend/scripts/start-saml-idp.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +if [[ ! -f idp-public-cert.pem ]]; then + echo "Generating new SAML Certificates" + openssl req \ + -x509 \ + -newkey rsa:1024 \ + -days 3650 \ + -nodes \ + -subj '/CN=localhost' \ + -keyout "idp-private-key.pem" \ + -out "idp-public-cert.pem" +fi + +echo "Downloading and starting SAML-IdP" +export NPM_CONFIG_REGISTRY=https://registry.npmjs.org +exec npx saml-idp --acsUrl "http://localhost:3003/auth/saml/handler/frame" --audience "http://localhost:3003" diff --git a/plugins/mock-idp-backend/gen-certs.sh b/plugins/mock-idp-backend/gen-certs.sh deleted file mode 100755 index d33c0f0e75..0000000000 --- a/plugins/mock-idp-backend/gen-certs.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -openssl req \ - -x509 \ - -newkey rsa:1024 \ - -days 3650 \ - -nodes \ - -subj '/CN=localhost' \ - -keyout "key.pem" \ - -out "cert.pem" diff --git a/plugins/mock-idp-backend/idp-private-key.pem b/plugins/mock-idp-backend/idp-private-key.pem deleted file mode 100644 index a90f813106..0000000000 --- a/plugins/mock-idp-backend/idp-private-key.pem +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMoHudQ6bah8Div+ -+6Ooi+ZchrseVWgXU+q/X6jIY7Gkf/aUMAYtiBAvr/FG5WTn1ePVdHxo20VkNRki -wFGeL0Z33IrGhkgkwpQZnWGwKpwqEddjed1ABeOksP/RFmzBCHNnxZGuxu5E1nqW -kPLjPRicAoo9V76xPLLlUJoS5ls7AgMBAAECgYEAk2hztGA1Zrutc3iGjvEJsrlM -LPeDBYIP3rZny7T62MDYBUg2iitqnbTNwVQjSC7IJ4a1iZjQdJ8bBw4OGP88Kfcv -QQl7PnU7+Sp/z6PMGpcsP/bBoStrqG4Bdv5bM15csLuGlkz/rQV/4Q/9nrCY4qDp -5U7Np/E2TTrjjPINtMECQQD53Q8hl8PkvH5pdqm+GwmGIQu1++WX8kD90ib7hnaL -Vs39pMGFviXmW8LLv0rwFhLNpVCNBpjh4jA8knKFYfexAkEAzv3t4whHUrzdxbBk -TXAg/JTg3KuiYNY64Pq5LzwSYgWNmbcD/FTjy3a7WQd6UzLpcZJ7armtwWaTOkWR -9rZoqwJBAJ/IxAJhgT5nZBehcM9HjwGdZFXObnaKzxECMTesN2bH7hcEI1WZ0bbM -e3e8Lvn1w7SKwUZOL7pT4TD7Hg06JyECQQDCwmpyk/eIAe0pdS7rLfXbsrlg6J2A -QBJmXYKgzwT89fymBW3anoU3jB/7RO30GpNMKWe2o765mqosygjs+fTBAkBdAny5 -Tixg8VNC32dbqlwzcZ8EvrgAoaGZc9HnS3Ay5/OUkMFvyjzNEF2I/brqe2wDPQg0 -nKpWXqNkODXwtYmF ------END PRIVATE KEY----- diff --git a/plugins/mock-idp-backend/idp-public-cert.pem b/plugins/mock-idp-backend/idp-public-cert.pem deleted file mode 100644 index 9fd9f66f97..0000000000 --- a/plugins/mock-idp-backend/idp-public-cert.pem +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBnzCCAQgCCQCIF7n8FCJ0azANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls -b2NhbGhvc3QwHhcNMjAwNjAxMTU0NDM1WhcNMzAwNTMwMTU0NDM1WjAUMRIwEAYD -VQQDDAlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMoHudQ6 -bah8Div++6Ooi+ZchrseVWgXU+q/X6jIY7Gkf/aUMAYtiBAvr/FG5WTn1ePVdHxo -20VkNRkiwFGeL0Z33IrGhkgkwpQZnWGwKpwqEddjed1ABeOksP/RFmzBCHNnxZGu -xu5E1nqWkPLjPRicAoo9V76xPLLlUJoS5ls7AgMBAAEwDQYJKoZIhvcNAQELBQAD -gYEAkG2oNLjFk0FUeM6cqnLvaA9fCml3JUA1nFgMshVXLvE/aOeXKhgy96DAcUw3 -xsdHHKMJpX4n49Odz3wyAtCWFTkz600T7oSrZ7FAU2VORKapBqqUL2HtLGB2gds+ -ndfiDwLV7Yb67cQikkgLU9ZzwYcGL6ovfxuw01f13ZyiAwg= ------END CERTIFICATE----- diff --git a/yarn.lock b/yarn.lock index 46dca51b6a..f869bbaa12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8075,7 +8075,7 @@ escape-goat@^2.0.0: resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= @@ -14130,7 +14130,21 @@ passport-oauth2@1.x.x: uid2 "0.0.x" utils-merge "1.x.x" -passport-strategy@1.x.x: +passport-saml@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-1.3.3.tgz#cbea1a2b21ff32b3bc4bfd84dc39c3a370df9935" + integrity sha512-54ecY/A6UEsyCehJws6a+J6THvwtYnGl9cnAUxx5DjsuKgZrDs0tSy58K4hCk1XG/LOcdQSF1TR3xlRXgTULhA== + dependencies: + debug "^3.1.0" + passport-strategy "*" + q "^1.5.0" + xml-crypto "^1.4.0" + xml-encryption "^1.0.0" + xml2js "0.4.x" + xmlbuilder "^11.0.0" + xmldom "0.1.x" + +passport-strategy@*, passport-strategy@1.x.x: version "1.0.0" resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= @@ -15118,7 +15132,7 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" -q@^1.1.2, q@^1.5.1: +q@^1.1.2, q@^1.5.0, q@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= @@ -16506,7 +16520,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sax@^1.2.4, sax@~1.2.4: +sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -19291,7 +19305,7 @@ xdg-basedir@^4.0.0: resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== -xml-crypto@^1.3.0: +xml-crypto@^1.3.0, xml-crypto@^1.4.0: version "1.5.3" resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.5.3.tgz#a8f500b90f0dfaf0efa3331c345ecb0fff993c34" integrity sha512-uHkmpUtX15xExe5iimPmakAZN+6CqIvjmaJTy4FwqGzaTjrKRBNeqMh8zGEzVNgW0dk6beFYpyQSgqV/J6C5xA== @@ -19318,6 +19332,16 @@ xml-encryption@0.11.2: xmldom "~0.1.15" xpath "0.0.27" +xml-encryption@^1.0.0: + version "1.2.0" + resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-1.2.0.tgz#37c8b470beae88b4625ea8cad82f108ea0f9c364" + integrity sha512-J3NjGMY8jf6bTo15jURTYBLtsisbnyCeM+MuxtfiAkZEZBnSZpNKjUUORhiOScKvSi6tMOAaZ3r7bZOXOni+Ew== + dependencies: + escape-html "^1.0.3" + node-forge "^0.7.0" + xmldom "~0.1.15" + xpath "0.0.27" + xml-formatter@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/xml-formatter/-/xml-formatter-2.1.0.tgz#ff438be6e2195e480b7525ecd3b06652ed76f390" @@ -19335,6 +19359,19 @@ xml-parser-xo@^3.0.0: resolved "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-3.0.0.tgz#4d46f1962e5100f228b5f73f34c61bb798430195" integrity sha512-MPPexqXBx48m3OFMQXxo7+RYhG6o6kCGflk4q4oL3uQ0b7d5NDKjHFDwUoozOTPT3WFztT13z3R9Sn0QCTIJcQ== +xml2js@0.4.x: + version "0.4.23" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@^11.0.0, xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" @@ -19345,6 +19382,11 @@ xmldom@0.1.27, xmldom@=0.1.27: resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= +xmldom@0.1.x, xmldom@~0.1.15: + version "0.1.31" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" + integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== + xmldom@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.3.0.tgz#e625457f4300b5df9c2e1ecb776147ece47f3e5a" @@ -19354,11 +19396,6 @@ xmldom@^0.3.0: version "0.1.19" resolved "https://codeload.github.com/auth0/xmldom/tar.gz/3376bc7beb5551bf68e12b0cc6b0e3669f77d392" -xmldom@~0.1.15: - version "0.1.31" - resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" - integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== - xpath@0.0.27: version "0.0.27" resolved "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92" From cd180f6928825f8c0ea29603311d2a4450d2d4cf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 11:33:56 +0200 Subject: [PATCH 84/97] plugins: remove mock-idp-backend --- plugins/mock-idp-backend/.eslintrc.js | 3 - plugins/mock-idp-backend/README.md | 7 - plugins/mock-idp-backend/package.json | 43 --- plugins/mock-idp-backend/src/index.ts | 17 -- plugins/mock-idp-backend/src/run.ts | 80 ------ plugins/mock-idp-backend/src/service/index.ts | 17 -- .../src/service/router.test.ts | 36 --- .../mock-idp-backend/src/service/router.ts | 34 --- plugins/mock-idp-backend/src/setupTests.ts | 17 -- plugins/mock-idp-backend/tsconfig.json | 15 -- yarn.lock | 246 +----------------- 11 files changed, 13 insertions(+), 502 deletions(-) delete mode 100644 plugins/mock-idp-backend/.eslintrc.js delete mode 100644 plugins/mock-idp-backend/README.md delete mode 100644 plugins/mock-idp-backend/package.json delete mode 100644 plugins/mock-idp-backend/src/index.ts delete mode 100644 plugins/mock-idp-backend/src/run.ts delete mode 100644 plugins/mock-idp-backend/src/service/index.ts delete mode 100644 plugins/mock-idp-backend/src/service/router.test.ts delete mode 100644 plugins/mock-idp-backend/src/service/router.ts delete mode 100644 plugins/mock-idp-backend/src/setupTests.ts delete mode 100644 plugins/mock-idp-backend/tsconfig.json diff --git a/plugins/mock-idp-backend/.eslintrc.js b/plugins/mock-idp-backend/.eslintrc.js deleted file mode 100644 index 16a033dbc6..0000000000 --- a/plugins/mock-idp-backend/.eslintrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; diff --git a/plugins/mock-idp-backend/README.md b/plugins/mock-idp-backend/README.md deleted file mode 100644 index c1925e326f..0000000000 --- a/plugins/mock-idp-backend/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Mock IdP Backend - -Mock backend for demonstrating 3rd party identity provider flow with SAML 2.0. - -## Links - -- (The Backstage homepage)[https://backstage.io] diff --git a/plugins/mock-idp-backend/package.json b/plugins/mock-idp-backend/package.json deleted file mode 100644 index 451004ca79..0000000000 --- a/plugins/mock-idp-backend/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@backstage/plugin-mock-idp-backend", - "version": "0.1.1-alpha.6", - "main": "dist", - "types": "src/index.ts", - "license": "Apache-2.0", - "private": true, - "scripts": { - "start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon dist/run.js\"", - "start:idp": "saml-idp --acsUrl=http://localhost:3003/auth/saml/handler/frame --audience http://localhost:3003", - "build": "tsc", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" - }, - "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.6", - "compression": "^1.7.4", - "cors": "^2.8.5", - "express": "^4.17.1", - "express-promise-router": "^3.0.3", - "fs-extra": "^9.0.0", - "helmet": "^3.22.0", - "morgan": "^1.10.0", - "saml-idp": "^1.2.1", - "winston": "^3.2.1", - "yn": "^4.0.0" - }, - "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@types/supertest": "^2.0.8", - "supertest": "^4.0.2", - "tsc-watch": "^4.2.3" - }, - "files": [ - "dist" - ], - "nodemonConfig": { - "watch": "./dist" - } -} diff --git a/plugins/mock-idp-backend/src/index.ts b/plugins/mock-idp-backend/src/index.ts deleted file mode 100644 index 7612c392a2..0000000000 --- a/plugins/mock-idp-backend/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export * from './service/router'; diff --git a/plugins/mock-idp-backend/src/run.ts b/plugins/mock-idp-backend/src/run.ts deleted file mode 100644 index ea67d335fd..0000000000 --- a/plugins/mock-idp-backend/src/run.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - errorHandler, - getRootLogger, - notFoundHandler, - requestLoggingHandler, -} from '@backstage/backend-common'; -import compression from 'compression'; -import cors from 'cors'; -import express from 'express'; -import helmet from 'helmet'; -import { Server } from 'http'; -import { Logger } from 'winston'; -import { createRouter } from './service'; - -export type ServerConfig = { - port: number; - logger: Logger; -}; - -function readConfig() { - const port = Number(process.env.PLUGIN_PORT) || 3003; - const logger = getRootLogger().child({ service: 'mock-idp-backend' }); - return { port, logger }; -} - -async function startStandaloneServer(config: ServerConfig): Promise { - const { port, logger } = config; - logger.debug('Creating application...'); - - const app = express(); - - app.use(helmet()); - app.use(cors()); - app.use(compression()); - app.use(express.json()); - app.use(requestLoggingHandler()); - app.use(await createRouter({ logger })); - app.use(notFoundHandler()); - app.use(errorHandler()); - - logger.debug('Starting application server...'); - - process.on('SIGINT', () => { - logger.info('CTRL+C pressed; exiting.'); - process.exit(0); - }); - - return await new Promise((resolve, reject) => { - const server = app.listen(port, (err?: Error) => { - if (err) { - reject(err); - return; - } - - logger.info(`Listening on port ${port}`); - resolve(server); - }); - }); -} - -startStandaloneServer(readConfig()).catch(err => { - console.error(err.stack || err); - process.exit(1); -}); diff --git a/plugins/mock-idp-backend/src/service/index.ts b/plugins/mock-idp-backend/src/service/index.ts deleted file mode 100644 index 38fbb697c4..0000000000 --- a/plugins/mock-idp-backend/src/service/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export { createRouter } from './router'; diff --git a/plugins/mock-idp-backend/src/service/router.test.ts b/plugins/mock-idp-backend/src/service/router.test.ts deleted file mode 100644 index 192baeebb9..0000000000 --- a/plugins/mock-idp-backend/src/service/router.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getVoidLogger } from '@backstage/backend-common'; -import { createRouter } from './router'; -import express from 'express'; -import request from 'supertest'; - -async function makeApp() { - const router = await createRouter({ logger: getVoidLogger() }); - const app = express(); - app.use(router); - return app; -} - -describe('router', () => { - it('should echo', async () => { - const app = await makeApp(); - const response = await request(app).get('/echo'); - expect(response.status).toEqual(200); - expect(response.text).toEqual('echo'); - }); -}); diff --git a/plugins/mock-idp-backend/src/service/router.ts b/plugins/mock-idp-backend/src/service/router.ts deleted file mode 100644 index 62bffb050d..0000000000 --- a/plugins/mock-idp-backend/src/service/router.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Router from 'express-promise-router'; -import { Logger } from 'winston'; - -export interface RouterOptions { - logger: Logger; -} - -export async function createRouter(options: RouterOptions) { - const { logger } = options; - const router = Router(); - - router.get('/echo', (_req, res) => { - logger.info('sending echo'); - res.send('echo'); - }); - - return router; -} diff --git a/plugins/mock-idp-backend/src/setupTests.ts b/plugins/mock-idp-backend/src/setupTests.ts deleted file mode 100644 index ba33cf996b..0000000000 --- a/plugins/mock-idp-backend/src/setupTests.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export {}; diff --git a/plugins/mock-idp-backend/tsconfig.json b/plugins/mock-idp-backend/tsconfig.json deleted file mode 100644 index 015a967f76..0000000000 --- a/plugins/mock-idp-backend/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "include": ["src"], - "compilerOptions": { - "outDir": "dist", - "incremental": true, - "sourceMap": true, - "declaration": true, - "strict": true, - "target": "es2019", - "module": "commonjs", - "esModuleInterop": true, - "lib": ["es2019"], - "types": ["node", "jest"] - } -} diff --git a/yarn.lock b/yarn.lock index f869bbaa12..760f1421b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,11 +2,6 @@ # yarn lockfile v1 -"@auth0/thumbprint@0.0.6": - version "0.0.6" - resolved "https://registry.npmjs.org/@auth0/thumbprint/-/thumbprint-0.0.6.tgz#cab1062c6c04662ce6c592d48157ec4268ae8518" - integrity sha1-yrEGLGwEZizmxZLUgVfsQmiuhRg= - "@babel/code-frame@7.5.5": version "7.5.5" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" @@ -4948,7 +4943,7 @@ async-limiter@~1.0.0: resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== -async@^2.1.5, async@^2.6.1, async@^2.6.2, async@~2.6.2: +async@^2.6.1, async@^2.6.2: version "2.6.3" resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -5458,7 +5453,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== -body-parser@1.19.0, body-parser@~1.19.0: +body-parser@1.19.0: version "1.19.0" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -7254,7 +7249,7 @@ debug@3.1.0, debug@=3.1.0: dependencies: ms "2.0.0" -debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.1: +debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -7844,12 +7839,7 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -ejs@2.6.1: - version "2.6.1" - resolved "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" - integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== - -ejs@^2.5.6, ejs@^2.7.4: +ejs@^2.7.4: version "2.7.4" resolved "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== @@ -8075,7 +8065,7 @@ escape-goat@^2.0.0: resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== -escape-html@^1.0.3, escape-html@~1.0.3: +escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= @@ -8525,20 +8515,6 @@ express-promise-router@^3.0.3: lodash.flattendeep "^4.0.0" methods "^1.0.0" -express-session@^1.17.1: - version "1.17.1" - resolved "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz#36ecbc7034566d38c8509885c044d461c11bf357" - integrity sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q== - dependencies: - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~2.0.0" - on-headers "~1.0.2" - parseurl "~1.3.3" - safe-buffer "5.2.0" - uid-safe "~2.1.5" - express@^4.17.0, express@^4.17.1: version "4.17.1" resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -8590,7 +8566,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: +extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -8965,15 +8941,6 @@ flatted@^2.0.0: resolved "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== -flowstate@^0.4.0: - version "0.4.1" - resolved "https://registry.npmjs.org/flowstate/-/flowstate-0.4.1.tgz#b5fbb8b7fc2d7bdc5b54be46c98309ef736f4ec0" - integrity sha1-tfu4t/wte9xbVL5GyYMJ73NvTsA= - dependencies: - clone "^1.0.2" - uid-safe "^2.1.0" - utils-flatten "^1.0.0" - flush-write-stream@^1.0.0: version "1.1.1" resolved "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" @@ -9030,11 +8997,6 @@ for-own@^1.0.0: dependencies: for-in "^1.0.1" -foreachasync@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" - integrity sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY= - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -9770,7 +9732,7 @@ handle-thing@^2.0.0: resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@4.7.6, handlebars@^4.4.0, handlebars@^4.7.3: +handlebars@^4.4.0, handlebars@^4.7.3: version "4.7.6" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== @@ -9906,14 +9868,6 @@ hastscript@^5.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" -hbs@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/hbs/-/hbs-4.1.1.tgz#8aab17ca6ae70f9aaa225278bed7af31011254b7" - integrity sha512-6QsbB4RwbpL4cb4DNyjEEPF+suwp+3yZqFVlhILEn92ScC0U4cDCR+FDX53jkfKJPhutcqhAvs+rOLZw5sQrDA== - dependencies: - handlebars "4.7.6" - walk "2.3.14" - he@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -13256,11 +13210,6 @@ node-forge@0.9.0: resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== -node-forge@^0.7.0: - version "0.7.6" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" - integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== - node-gyp@^5.0.2: version "5.1.0" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz#8e31260a7af4a2e2f994b0673d4e0b3866156332" @@ -14130,21 +14079,7 @@ passport-oauth2@1.x.x: uid2 "0.0.x" utils-merge "1.x.x" -passport-saml@^1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-1.3.3.tgz#cbea1a2b21ff32b3bc4bfd84dc39c3a370df9935" - integrity sha512-54ecY/A6UEsyCehJws6a+J6THvwtYnGl9cnAUxx5DjsuKgZrDs0tSy58K4hCk1XG/LOcdQSF1TR3xlRXgTULhA== - dependencies: - debug "^3.1.0" - passport-strategy "*" - q "^1.5.0" - xml-crypto "^1.4.0" - xml-encryption "^1.0.0" - xml2js "0.4.x" - xmlbuilder "^11.0.0" - xmldom "0.1.x" - -passport-strategy@*, passport-strategy@1.x.x: +passport-strategy@1.x.x: version "1.0.0" resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= @@ -15132,7 +15067,7 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" -q@^1.1.2, q@^1.5.0, q@^1.5.1: +q@^1.1.2, q@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= @@ -15195,11 +15130,6 @@ ramda@^0.21.0: resolved "https://registry.npmjs.org/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" integrity sha1-oAGr7bP/YQd9T/HVd9RN536NCjU= -random-bytes@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" - integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= - randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -16438,7 +16368,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.0, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -16460,51 +16390,6 @@ safe-regex@^1.1.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -saml-idp@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/saml-idp/-/saml-idp-1.2.1.tgz#2df394cd406ce273641115f587062ea60d29ce03" - integrity sha512-C7iXTxryohn8fOWUGyPDCJwyA4eWyh4gJrMAndyRe+idTrj51kPOW1FAAsIBc7afmoJLM00qqcAP/gxalq4r9A== - dependencies: - body-parser "~1.19.0" - chalk "^4.0.0" - debug "~4.1.1" - express "^4.17.1" - express-session "^1.17.1" - extend "^3.0.2" - hbs "^4.1.1" - morgan "^1.10.0" - samlp "github:mcguinness/node-samlp" - xml-formatter "^2.1.0" - xmldom "^0.3.0" - yargs "^15.3.1" - -"saml@github:mcguinness/node-saml": - version "0.12.5" - resolved "https://codeload.github.com/mcguinness/node-saml/tar.gz/ec47b9ab43ad756a5d1fbc82c71e260f7a5cb18a" - dependencies: - async "~2.6.2" - moment "2.24.0" - valid-url "~1.0.9" - xml-crypto "~1.3.0" - xml-encryption "0.11.2" - xml-name-validator "~3.0.0" - xmldom "=0.1.27" - xpath "0.0.27" - -"samlp@github:mcguinness/node-samlp": - version "3.4.1" - resolved "https://codeload.github.com/mcguinness/node-samlp/tar.gz/7bfb7c29be520f249beff6a9e933b63a628e31a1" - dependencies: - "@auth0/thumbprint" "0.0.6" - ejs "2.6.1" - flowstate "^0.4.0" - querystring "^0.2.0" - saml "github:mcguinness/node-saml" - xml-crypto "^1.3.0" - xmldom "github:auth0/xmldom#v0.1.19-auth0_1" - xpath "0.0.27" - xtend "^4.0.1" - sane@^4.0.3: version "4.1.0" resolved "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" @@ -16520,7 +16405,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: +sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -18339,13 +18224,6 @@ uid-number@0.0.6: resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= -uid-safe@^2.1.0, uid-safe@~2.1.5: - version "2.1.5" - resolved "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" - integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== - dependencies: - random-bytes "~1.0.0" - uid2@0.0.x: version "0.0.3" resolved "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" @@ -18696,11 +18574,6 @@ utila@^0.4.0, utila@~0.4: resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= -utils-flatten@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/utils-flatten/-/utils-flatten-1.0.0.tgz#01f30d3193be464c40b31755e6740d0db0cef243" - integrity sha1-AfMNMZO+RkxAsxdV5nQNDbDO8kM= - utils-merge@1.0.1, utils-merge@1.x.x: version "1.0.1" resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -18742,7 +18615,7 @@ v8flags@^3.1.3: dependencies: homedir-polyfill "^1.0.1" -valid-url@1.0.9, valid-url@~1.0.9: +valid-url@1.0.9: version "1.0.9" resolved "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= @@ -18849,13 +18722,6 @@ wait-on@4.0.0: request-promise-native "^1.0.8" rxjs "^6.5.4" -walk@2.3.14: - version "2.3.14" - resolved "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz#60ec8631cfd23276ae1e7363ce11d626452e1ef3" - integrity sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg== - dependencies: - foreachasync "^3.0.0" - walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" @@ -19305,102 +19171,16 @@ xdg-basedir@^4.0.0: resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== -xml-crypto@^1.3.0, xml-crypto@^1.4.0: - version "1.5.3" - resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.5.3.tgz#a8f500b90f0dfaf0efa3331c345ecb0fff993c34" - integrity sha512-uHkmpUtX15xExe5iimPmakAZN+6CqIvjmaJTy4FwqGzaTjrKRBNeqMh8zGEzVNgW0dk6beFYpyQSgqV/J6C5xA== - dependencies: - xmldom "0.1.27" - xpath "0.0.27" - -xml-crypto@~1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.3.0.tgz#5450e0768c24a854a5cfea6c485d2b73c835d9e1" - integrity sha512-Kx/owhke7oy89NAB8HTkaENc1BaCixQDHD6Wg61VTIOdjBlIRLNs2Ts76MhJz78EPyOMoqUoY4ytShCqbv1XBA== - dependencies: - xmldom "0.1.27" - xpath "0.0.27" - -xml-encryption@0.11.2: - version "0.11.2" - resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-0.11.2.tgz#c217f5509547e34b500b829f2c0bca85cca73a21" - integrity sha512-jVvES7i5ovdO7N+NjgncA326xYKjhqeAnnvIgRnY7ROLCfFqEDLwP0Sxp/30SHG0AXQV1048T5yinOFyvwGFzg== - dependencies: - async "^2.1.5" - ejs "^2.5.6" - node-forge "^0.7.0" - xmldom "~0.1.15" - xpath "0.0.27" - -xml-encryption@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-1.2.0.tgz#37c8b470beae88b4625ea8cad82f108ea0f9c364" - integrity sha512-J3NjGMY8jf6bTo15jURTYBLtsisbnyCeM+MuxtfiAkZEZBnSZpNKjUUORhiOScKvSi6tMOAaZ3r7bZOXOni+Ew== - dependencies: - escape-html "^1.0.3" - node-forge "^0.7.0" - xmldom "~0.1.15" - xpath "0.0.27" - -xml-formatter@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/xml-formatter/-/xml-formatter-2.1.0.tgz#ff438be6e2195e480b7525ecd3b06652ed76f390" - integrity sha512-t55v5mfpohwKvNbfd8A0FZSZI22//hqXqx3AwRx3mjZel0IEoRM2p1bvVnvNPxHqdqZ0sDjUrBqfHJNbIfE8fw== - dependencies: - xml-parser-xo "^3.0.0" - -xml-name-validator@^3.0.0, xml-name-validator@~3.0.0: +xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml-parser-xo@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-3.0.0.tgz#4d46f1962e5100f228b5f73f34c61bb798430195" - integrity sha512-MPPexqXBx48m3OFMQXxo7+RYhG6o6kCGflk4q4oL3uQ0b7d5NDKjHFDwUoozOTPT3WFztT13z3R9Sn0QCTIJcQ== - -xml2js@0.4.x: - version "0.4.23" - resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@^11.0.0, xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmldom@0.1.27, xmldom@=0.1.27: - version "0.1.27" - resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" - integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= - -xmldom@0.1.x, xmldom@~0.1.15: - version "0.1.31" - resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" - integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== - -xmldom@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.3.0.tgz#e625457f4300b5df9c2e1ecb776147ece47f3e5a" - integrity sha512-z9s6k3wxE+aZHgXYxSTpGDo7BYOUfJsIRyoZiX6HTjwpwfS2wpQBQKa2fD+ShLyPkqDYo5ud7KitmLZ2Cd6r0g== - -"xmldom@github:auth0/xmldom#v0.1.19-auth0_1": - version "0.1.19" - resolved "https://codeload.github.com/auth0/xmldom/tar.gz/3376bc7beb5551bf68e12b0cc6b0e3669f77d392" - -xpath@0.0.27: - version "0.0.27" - resolved "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92" - integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ== - xregexp@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" From 17a16f617c0e63d2de078f2f68f6f1d534906459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 2 Jun 2020 13:35:59 +0200 Subject: [PATCH 85/97] Break out the location refresh loop as well --- packages/backend/src/plugins/catalog.ts | 9 +- .../src/catalog/DatabaseEntitiesCatalog.ts | 2 +- .../src/catalog/DatabaseLocationsCatalog.ts | 29 +- plugins/catalog-backend/src/catalog/types.ts | 6 + .../src/database/DatabaseManager.test.ts | 248 ------------------ .../src/database/DatabaseManager.ts | 184 +------------ .../ingestion/HigherOrderOperations.test.ts | 153 ++++++++++- .../src/ingestion/HigherOrderOperations.ts | 155 +++++++++++ .../src/service/router.test.ts | 2 + 9 files changed, 347 insertions(+), 441 deletions(-) delete mode 100644 plugins/catalog-backend/src/database/DatabaseManager.test.ts diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 150b9d1826..906e8c2d11 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -32,7 +32,6 @@ export default async function createPlugin({ logger, database, }: PluginEnvironment) { - const policy = new EntityPolicies(); const ingestionModel = new IngestionModels( new LocationReaders(), new DescriptorParsers(), @@ -40,19 +39,17 @@ export default async function createPlugin({ ); const db = await DatabaseManager.createDatabase(database, logger); - runPeriodically( - () => DatabaseManager.refreshLocations(db, ingestionModel, policy, logger), - 10000, - ); - const entitiesCatalog = new DatabaseEntitiesCatalog(db); const locationsCatalog = new DatabaseLocationsCatalog(db); const higherOrderOperation = new HigherOrderOperations( entitiesCatalog, locationsCatalog, ingestionModel, + logger, ); + runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000); + return await createRouter({ entitiesCatalog, locationsCatalog, diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 6a7a51a168..1ec1ebf6e5 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -38,8 +38,8 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { async entityByName( kind: string, - name: string, namespace: string | undefined, + name: string, ): Promise { return await this.database.transaction(tx => this.entityByNameInternal(tx, kind, name, namespace), diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index 066fab8464..a1e76679f7 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -16,7 +16,10 @@ import { Location } from '@backstage/catalog-model'; import type { Database } from '../database'; -import { DatabaseLocationUpdateLogEvent } from '../database/types'; +import { + DatabaseLocationUpdateLogEvent, + DatabaseLocationUpdateLogStatus, +} from '../database/types'; import { LocationResponse, LocationsCatalog } from './types'; export class DatabaseLocationsCatalog implements LocationsCatalog { @@ -63,4 +66,28 @@ export class DatabaseLocationsCatalog implements LocationsCatalog { data, }; } + + async logUpdateSuccess( + locationId: string, + entityName?: string, + ): Promise { + await this.database.addLocationUpdateLogEvent( + locationId, + DatabaseLocationUpdateLogStatus.SUCCESS, + entityName, + ); + } + + async logUpdateFailure( + locationId: string, + error?: Error, + entityName?: string, + ): Promise { + await this.database.addLocationUpdateLogEvent( + locationId, + DatabaseLocationUpdateLogStatus.FAIL, + entityName, + error?.message, + ); + } } diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 0499b8a409..b0d3dde0fb 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -62,4 +62,10 @@ export type LocationsCatalog = { locations(): Promise; location(id: string): Promise; locationHistory(id: string): Promise; + logUpdateSuccess(locationId: string, entityName?: string): Promise; + logUpdateFailure( + locationId: string, + error?: Error, + entityName?: string, + ): Promise; }; diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts deleted file mode 100644 index 0c28524f8d..0000000000 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getVoidLogger } from '@backstage/backend-common'; -import type { Entity, EntityPolicy } from '@backstage/catalog-model'; -import Knex from 'knex'; -import type { IngestionModel } from '../ingestion/types'; -import { DatabaseManager } from './DatabaseManager'; -import { DatabaseLocationUpdateLogStatus } from './types'; -import type { - Database, - DbLocationsRow, - DbLocationsRowWithStatus, -} from './types'; - -describe('DatabaseManager', () => { - describe('refreshLocations', () => { - it('works with no locations added', async () => { - const db = ({ - locations: jest.fn().mockResolvedValue([]), - } as unknown) as Database; - const reader: IngestionModel = { - readLocation: jest.fn(), - }; - const policy: EntityPolicy = { - enforce: jest.fn(), - }; - - await expect( - DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), - ).resolves.toBeUndefined(); - expect(reader.readLocation).not.toHaveBeenCalled(); - expect(policy.enforce).not.toHaveBeenCalled(); - }); - - it('can update a single location', async () => { - const location: DbLocationsRowWithStatus = { - id: '123', - type: 'some', - target: 'thing', - message: '', - status: DatabaseLocationUpdateLogStatus.SUCCESS, - timestamp: new Date(314159265).toISOString(), - }; - const desc: Entity = { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { name: 'c1' }, - spec: { type: 'service' }, - }; - - const tx = (undefined as unknown) as Knex.Transaction; - - const db = ({ - transaction: jest.fn(f => f(tx)), - entity: jest.fn(() => Promise.resolve(undefined)), - addEntity: jest.fn(), - locations: jest.fn(() => Promise.resolve([location])), - addLocationUpdateLogEvent: jest.fn(), - } as Partial) as Database; - - const reader: IngestionModel = { - readLocation: jest.fn(() => - Promise.resolve([{ type: 'data', data: desc }]), - ), - }; - const policy: EntityPolicy = { - enforce: jest.fn(() => Promise.resolve(desc)), - }; - - await expect( - DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), - ).resolves.toBeUndefined(); - expect(reader.readLocation).toHaveBeenCalledTimes(1); - expect(reader.readLocation).toHaveBeenNthCalledWith(1, 'some', 'thing'); - expect(db.addEntity).toHaveBeenCalledTimes(1); - expect(db.addEntity).toHaveBeenNthCalledWith(1, undefined, { - locationId: '123', - entity: expect.objectContaining({ - metadata: expect.objectContaining({ name: 'c1' }), - }), - }); - }); - - it('logs successful updates', async () => { - const tx = (undefined as unknown) as Knex.Transaction; - - const db = ({ - transaction: jest.fn(f => f(tx)), - addEntity: jest.fn(), - entity: jest.fn(() => Promise.resolve(undefined)), - locations: jest.fn(() => - Promise.resolve([ - { - id: '123', - type: 'some', - target: 'thing', - } as DbLocationsRow, - ]), - ), - addLocationUpdateLogEvent: jest.fn(), - } as unknown) as Database; - - const desc: Entity = { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { name: 'c1' }, - spec: { type: 'service' }, - }; - const reader: IngestionModel = { - readLocation: jest.fn(() => - Promise.resolve([{ type: 'data', data: desc }]), - ), - }; - const policy: EntityPolicy = { - enforce: jest.fn(() => Promise.resolve(desc)), - }; - - await expect( - DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), - ).resolves.toBeUndefined(); - - expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( - 1, - '123', - DatabaseLocationUpdateLogStatus.SUCCESS, - 'c1', - ); - - expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( - 2, - '123', - DatabaseLocationUpdateLogStatus.SUCCESS, - undefined, - ); - }); - - it('logs unsuccessful updates when parser fails', async () => { - const tx = (undefined as unknown) as Knex.Transaction; - - const db = ({ - transaction: jest.fn(f => f(tx)), - addEntity: jest.fn(), - locations: jest.fn(() => - Promise.resolve([ - { - id: '123', - type: 'some', - target: 'thing', - } as DbLocationsRow, - ]), - ), - addLocationUpdateLogEvent: jest.fn(), - } as unknown) as Database; - - const desc: Entity = { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { name: 'c1' }, - spec: { type: 'service' }, - }; - const reader: IngestionModel = { - readLocation: jest.fn(() => - Promise.resolve([{ type: 'data', data: desc }]), - ), - }; - const policy: EntityPolicy = { - enforce: jest.fn(() => - Promise.reject(new Error('parser error message')), - ), - }; - - await expect( - DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), - ).resolves.toBeUndefined(); - - expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( - 1, - '123', - DatabaseLocationUpdateLogStatus.FAIL, - 'c1', - 'parser error message', - ); - - expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( - 2, - '123', - DatabaseLocationUpdateLogStatus.SUCCESS, - undefined, - ); - }); - - it('logs unsuccessful updates when reader fails', async () => { - const tx = (undefined as unknown) as Knex.Transaction; - - const db = ({ - transaction: jest.fn(f => f(tx)), - addEntity: jest.fn(), - locations: jest.fn(() => - Promise.resolve([ - { - id: '123', - type: 'some', - target: 'thing', - } as DbLocationsRow, - ]), - ), - addLocationUpdateLogEvent: jest.fn(), - } as unknown) as Database; - - const reader: IngestionModel = { - readLocation: jest.fn(() => - Promise.reject([{ type: 'error', error: new Error('test message') }]), - ), - }; - const policy: EntityPolicy = { - enforce: jest.fn(() => - Promise.reject(new Error('parser error message')), - ), - }; - - await expect( - DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), - ).resolves.toBeUndefined(); - - expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( - 1, - '123', - DatabaseLocationUpdateLogStatus.FAIL, - undefined, - undefined, - ); - }); - }); -}); diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index 4268dc4146..bc7c7b8dae 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -14,15 +14,11 @@ * limitations under the License. */ -import type { Entity, EntityPolicy } from '@backstage/catalog-model'; import Knex from 'knex'; -import lodash from 'lodash'; import path from 'path'; import { Logger } from 'winston'; -import type { IngestionModel } from '../ingestion/types'; import { CommonDatabase } from './CommonDatabase'; -import { DatabaseLocationUpdateLogStatus } from './types'; -import type { Database, DbEntityRequest } from './types'; +import type { Database } from './types'; export class DatabaseManager { public static async createDatabase( @@ -35,182 +31,4 @@ export class DatabaseManager { }); return new CommonDatabase(knex, logger); } - - private static async logUpdateSuccess( - database: Database, - locationId: string, - entityName?: string, - ) { - return database.addLocationUpdateLogEvent( - locationId, - DatabaseLocationUpdateLogStatus.SUCCESS, - entityName, - ); - } - - private static async logUpdateFailure( - database: Database, - locationId: string, - error?: Error, - entityName?: string, - ) { - return database.addLocationUpdateLogEvent( - locationId, - DatabaseLocationUpdateLogStatus.FAIL, - entityName, - error?.message, - ); - } - - public static async refreshLocations( - database: Database, - ingestionModel: IngestionModel, - entityPolicy: EntityPolicy, - logger: Logger, - ): Promise { - const locations = await database.locations(); - for (const location of locations) { - try { - logger.debug( - `Refreshing location id="${location.id}" type="${location.type}" target="${location.target}"`, - ); - - const readerOutput = await ingestionModel.readLocation( - location.type, - location.target, - ); - - for (const readerItem of readerOutput) { - if (readerItem.type === 'error') { - logger.info(readerItem.error); - continue; - } - - try { - const entity = await entityPolicy.enforce(readerItem.data); - await DatabaseManager.refreshSingleEntity( - database, - location.id, - entity, - logger, - ); - await DatabaseManager.logUpdateSuccess( - database, - location.id, - entity.metadata.name, - ); - } catch (error) { - await DatabaseManager.logUpdateFailure( - database, - location.id, - error, - readerItem.data.metadata.name, - ); - } - } - await DatabaseManager.logUpdateSuccess( - database, - location.id, - undefined, - ); - } catch (error) { - logger.debug( - `Failed to refresh location id="${location.id}", ${error}`, - ); - await DatabaseManager.logUpdateFailure(database, location.id, error); - } - } - } - - private static async refreshSingleEntity( - database: Database, - locationId: string, - entity: Entity, - logger: Logger, - ): Promise { - const { kind } = entity; - const { name, namespace } = entity.metadata || {}; - if (!name) { - throw new Error('Entities without names are not yet supported'); - } - - const request: DbEntityRequest = { - locationId: locationId, - entity: entity, - }; - - logger.debug( - `Read entity kind="${kind}" name="${name}" namespace="${namespace}"`, - ); - - await database.transaction(async tx => { - const previous = await database.entity(tx, kind, name, namespace); - if (!previous) { - logger.debug(`No such entity found, adding`); - await database.addEntity(tx, request); - } else if ( - !DatabaseManager.entitiesAreEqual(previous.entity, request.entity) - ) { - logger.debug(`Different from existing entity, updating`); - await database.updateEntity(tx, request); - } else { - logger.debug(`Equal to existing entity, skipping update`); - } - }); - } - - private static entitiesAreEqual(previous: Entity, next: Entity) { - if ( - previous.apiVersion !== next.apiVersion || - previous.kind !== next.kind || - !lodash.isEqual(previous.spec, next.spec) // Accept that {} !== undefined - ) { - return false; - } - - // Since the next annotations get merged into the previous, extract only - // the overlapping keys and check if their values match. - if (next.metadata.annotations) { - if (!previous.metadata.annotations) { - return false; - } - if ( - !lodash.isEqual( - next.metadata.annotations, - lodash.pick( - previous.metadata.annotations, - Object.keys(next.metadata.annotations), - ), - ) - ) { - return false; - } - } - - const e1 = lodash.cloneDeep(previous); - const e2 = lodash.cloneDeep(next); - - if (!e1.metadata.labels) { - e1.metadata.labels = {}; - } - if (!e2.metadata.labels) { - e2.metadata.labels = {}; - } - - // Remove generated fields - delete e1.metadata.uid; - delete e1.metadata.etag; - delete e1.metadata.generation; - delete e2.metadata.uid; - delete e2.metadata.etag; - delete e2.metadata.generation; - - // Remove already compared things - delete e1.metadata.annotations; - delete e1.spec; - delete e2.metadata.annotations; - delete e2.spec; - - return lodash.isEqual(e1, e2); - } } diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts index 7f1e890fbf..bd8dbedb27 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts @@ -14,10 +14,13 @@ * limitations under the License. */ +import { getVoidLogger } from '@backstage/backend-common'; +import { Entity, Location } from '@backstage/catalog-model'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; -import { IngestionModel } from './types'; +import { LocationUpdateStatus } from '../catalog/types'; +import { DatabaseLocationUpdateLogStatus } from '../database/types'; import { HigherOrderOperations } from './HigherOrderOperations'; -import { Entity } from '@backstage/catalog-model'; +import { IngestionModel } from './types'; describe('HigherOrderOperations', () => { let entitiesCatalog: jest.Mocked; @@ -39,6 +42,8 @@ describe('HigherOrderOperations', () => { locations: jest.fn(), location: jest.fn(), locationHistory: jest.fn(), + logUpdateSuccess: jest.fn(), + logUpdateFailure: jest.fn(), }; ingestionModel = { readLocation: jest.fn(), @@ -47,6 +52,7 @@ describe('HigherOrderOperations', () => { entitiesCatalog, locationsCatalog, ingestionModel, + getVoidLogger(), ); }); @@ -140,4 +146,147 @@ describe('HigherOrderOperations', () => { expect(locationsCatalog.addLocation).not.toBeCalled(); }); }); + + describe('refreshLocations', () => { + it('works with no locations added', async () => { + locationsCatalog.locations.mockResolvedValue([]); + + await expect( + higherOrderOperation.refreshAllLocations(), + ).resolves.toBeUndefined(); + + expect(locationsCatalog.locations).toHaveBeenCalledTimes(1); + expect(ingestionModel.readLocation).not.toHaveBeenCalled(); + expect(entitiesCatalog.addOrUpdateEntity).not.toHaveBeenCalled(); + }); + + it('can update a single location where a matching entity did not exist', async () => { + const locationStatus: LocationUpdateStatus = { + message: '', + status: DatabaseLocationUpdateLogStatus.SUCCESS, + timestamp: new Date(314159265).toISOString(), + }; + const location: Location = { + id: '123', + type: 'some', + target: 'thing', + }; + const desc: Entity = { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { name: 'c1' }, + spec: { type: 'service' }, + }; + + locationsCatalog.locations.mockResolvedValue([ + { currentStatus: locationStatus, data: location }, + ]); + ingestionModel.readLocation.mockResolvedValue([ + { type: 'data', data: desc }, + ]); + entitiesCatalog.entityByName.mockResolvedValue(undefined); + entitiesCatalog.addOrUpdateEntity.mockResolvedValue(desc); + + await expect( + higherOrderOperation.refreshAllLocations(), + ).resolves.toBeUndefined(); + + expect(locationsCatalog.locations).toHaveBeenCalledTimes(1); + expect(ingestionModel.readLocation).toHaveBeenCalledTimes(1); + expect(ingestionModel.readLocation).toHaveBeenNthCalledWith( + 1, + 'some', + 'thing', + ); + expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entityByName).toHaveBeenNthCalledWith( + 1, + 'Component', + undefined, + 'c1', + ); + expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + metadata: expect.objectContaining({ name: 'c1' }), + }), + '123', + ); + }); + + it('logs successful updates', async () => { + const locationStatus: LocationUpdateStatus = { + message: '', + status: DatabaseLocationUpdateLogStatus.SUCCESS, + timestamp: new Date(314159265).toISOString(), + }; + const location: Location = { + id: '123', + type: 'some', + target: 'thing', + }; + const desc: Entity = { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { name: 'c1' }, + spec: { type: 'service' }, + }; + + locationsCatalog.locations.mockResolvedValue([ + { currentStatus: locationStatus, data: location }, + ]); + ingestionModel.readLocation.mockResolvedValue([ + { type: 'data', data: desc }, + ]); + entitiesCatalog.entityByName.mockResolvedValue(undefined); + entitiesCatalog.addOrUpdateEntity.mockResolvedValue(desc); + + await expect( + higherOrderOperation.refreshAllLocations(), + ).resolves.toBeUndefined(); + + expect(locationsCatalog.logUpdateSuccess).toHaveBeenCalledTimes(2); + expect(locationsCatalog.logUpdateSuccess).toHaveBeenCalledWith( + '123', + undefined, + ); + expect(locationsCatalog.logUpdateSuccess).toHaveBeenCalledWith( + '123', + 'c1', + ); + }); + + it('logs unsuccessful updates when reader fails', async () => { + const locationStatus: LocationUpdateStatus = { + message: '', + status: DatabaseLocationUpdateLogStatus.SUCCESS, + timestamp: new Date(314159265).toISOString(), + }; + const location: Location = { + id: '123', + type: 'some', + target: 'thing', + }; + + locationsCatalog.locations.mockResolvedValue([ + { currentStatus: locationStatus, data: location }, + ]); + ingestionModel.readLocation.mockRejectedValue( + new Error('reader error message'), + ); + + await expect( + higherOrderOperation.refreshAllLocations(), + ).resolves.toBeUndefined(); + + expect(ingestionModel.readLocation).toHaveBeenCalledTimes(1); + expect(locationsCatalog.logUpdateFailure).toHaveBeenCalledTimes(1); + expect(locationsCatalog.logUpdateSuccess).not.toHaveBeenCalled(); + expect(locationsCatalog.logUpdateFailure).toHaveBeenCalledWith( + '123', + expect.objectContaining({ message: 'reader error message' }), + ); + }); + }); }); diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index e28d1e722a..e9051d1f78 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -16,10 +16,12 @@ import { InputError } from '@backstage/backend-common'; import { Entity, Location, LocationSpec } from '@backstage/catalog-model'; +import lodash from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { IngestionModel } from '../ingestion'; import { AddLocationResult, HigherOrderOperation } from './types'; +import { Logger } from 'winston'; const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; @@ -34,15 +36,18 @@ export class HigherOrderOperations implements HigherOrderOperation { private readonly entitiesCatalog: EntitiesCatalog; private readonly locationsCatalog: LocationsCatalog; private readonly ingestionModel: IngestionModel; + private readonly logger: Logger; constructor( entitiesCatalog: EntitiesCatalog, locationsCatalog: LocationsCatalog, ingestionModel: IngestionModel, + logger: Logger, ) { this.entitiesCatalog = entitiesCatalog; this.locationsCatalog = locationsCatalog; this.ingestionModel = ingestionModel; + this.logger = logger; } /** @@ -111,4 +116,154 @@ export class HigherOrderOperations implements HigherOrderOperation { return { location, entities: outputEntities }; } + + /** + * Goes through all registered locations, and performs a refresh of each one. + * + * Entities are read from their respective sources, are parsed and validated + * according to the entity policy, and get inserted or updated in the catalog. + * Entities that have disappeared from their location are left orphaned, + * without changes. + */ + async refreshAllLocations(): Promise { + const startTimestamp = new Date().valueOf(); + this.logger.info('Beginning locations refresh'); + + const locations = await this.locationsCatalog.locations(); + this.logger.info(`Visiting ${locations.length} locations`); + + for (const { data: location } of locations) { + this.logger.debug( + `Refreshing location id="${location.id}" type="${location.type}" target="${location.target}"`, + ); + try { + await this.refreshSingleLocation(location); + await this.locationsCatalog.logUpdateSuccess(location.id, undefined); + } catch (e) { + this.logger.debug( + `Failed to refresh location id="${location.id}" type="${location.type}" target="${location.target}", ${e}`, + ); + await this.locationsCatalog.logUpdateFailure(location.id, e); + } + } + + const endTimestamp = new Date().valueOf(); + const duration = ((endTimestamp - startTimestamp) / 1000).toFixed(1); + this.logger.debug(`Completed locations refresh in ${duration} seconds`); + } + + // Performs a full refresh of a single location + private async refreshSingleLocation(location: Location) { + const readerOutput = await this.ingestionModel.readLocation( + location.type, + location.target, + ); + + for (const readerItem of readerOutput) { + if (readerItem.type === 'error') { + this.logger.debug( + `Failed item in location id="${location.id}" type="${location.type}" target="${location.target}", ${readerItem.error}`, + ); + continue; + } + + const entity = readerItem.data; + this.logger.debug( + `Read entity kind="${entity.kind}" name="${ + entity.metadata.name + }" namespace="${entity.metadata.namespace || ''}"`, + ); + + try { + const previous = await this.entitiesCatalog.entityByName( + entity.kind, + entity.metadata.namespace, + entity.metadata.name, + ); + + if (!previous) { + this.logger.debug(`No such entity found, adding`); + await this.entitiesCatalog.addOrUpdateEntity(entity, location.id); + } else if (!this.entitiesAreEqual(previous, entity)) { + this.logger.debug(`Different from existing entity, updating`); + await this.entitiesCatalog.addOrUpdateEntity(entity, location.id); + } else { + this.logger.debug(`Equal to existing entity, skipping update`); + } + + await this.locationsCatalog.logUpdateSuccess( + location.id, + entity.metadata.name, + ); + } catch (error) { + this.logger.debug( + `Failed refresh of entity kind="${entity.kind}" name="${ + entity.metadata.name + }" namespace="${entity.metadata.namespace || ''}", ${error}`, + ); + + await this.locationsCatalog.logUpdateFailure( + location.id, + error, + entity.metadata.name, + ); + } + } + } + + // Compares entities, ignoring generated and irrelevant data + private entitiesAreEqual(previous: Entity, next: Entity): boolean { + if ( + previous.apiVersion !== next.apiVersion || + previous.kind !== next.kind || + !lodash.isEqual(previous.spec, next.spec) // Accept that {} !== undefined + ) { + return false; + } + + // Since the next annotations get merged into the previous, extract only + // the overlapping keys and check if their values match. + if (next.metadata.annotations) { + if (!previous.metadata.annotations) { + return false; + } + if ( + !lodash.isEqual( + next.metadata.annotations, + lodash.pick( + previous.metadata.annotations, + Object.keys(next.metadata.annotations), + ), + ) + ) { + return false; + } + } + + const e1 = lodash.cloneDeep(previous); + const e2 = lodash.cloneDeep(next); + + if (!e1.metadata.labels) { + e1.metadata.labels = {}; + } + if (!e2.metadata.labels) { + e2.metadata.labels = {}; + } + + // Remove generated fields + delete e1.metadata.uid; + delete e1.metadata.etag; + delete e1.metadata.generation; + delete e2.metadata.uid; + delete e2.metadata.etag; + delete e2.metadata.generation; + + // Remove already compared things + delete e1.metadata.annotations; + delete e1.spec; + delete e2.metadata.annotations; + delete e2.spec; + + return lodash.isEqual(e1, e2); + } } diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index d26476bca0..0efc0a3be4 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -43,6 +43,8 @@ describe('createRouter', () => { locations: jest.fn(), location: jest.fn(), locationHistory: jest.fn(), + logUpdateSuccess: jest.fn(), + logUpdateFailure: jest.fn(), }; higherOrderOperation = { addLocation: jest.fn(), From 3c47bc4e23ef8b1e31633d35a921e499c4fefd7e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 12:00:30 +0200 Subject: [PATCH 86/97] plugins/auth-backend: refactor to allow non-oauth providers --- .../auth-backend/src/providers/factories.ts | 57 +++++++++---------- .../src/providers/github/index.ts | 2 +- .../src/providers/github/provider.ts | 7 +++ .../src/providers/google/index.ts | 2 +- .../src/providers/google/provider.ts | 7 +++ .../auth-backend/src/providers/index.test.ts | 23 -------- plugins/auth-backend/src/providers/index.ts | 22 +------ plugins/auth-backend/src/providers/types.ts | 10 +--- plugins/auth-backend/src/service/router.ts | 9 +-- 9 files changed, 52 insertions(+), 87 deletions(-) delete mode 100644 plugins/auth-backend/src/providers/index.test.ts diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 0c9f99e878..010b86f409 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -14,37 +14,34 @@ * limitations under the License. */ -import { - AuthProviderFactories, - AuthProviderRouteHandlers, - AuthProviderConfig, -} from './types'; -import { GoogleAuthProvider } from './google'; -import { GithubAuthProvider } from './github'; -import { OAuthProvider } from './OAuthProvider'; +import Router from 'express-promise-router'; +import { createGithubProvider } from './github'; +import { createGoogleProvider } from './google'; +import { AuthProviderFactory, AuthProviderConfig } from './types'; -export class ProviderFactories { - private static readonly providerFactories: AuthProviderFactories = { - google: GoogleAuthProvider, - github: GithubAuthProvider, - }; +const factories: { [providerId: string]: AuthProviderFactory } = { + google: createGoogleProvider, + github: createGithubProvider, +}; - public static getProviderFactory( - config: AuthProviderConfig, - ): AuthProviderRouteHandlers { - const providerId = config.provider; - const ProviderImpl = ProviderFactories.providerFactories[providerId]; - if (!ProviderImpl) { - throw Error( - `Provider Implementation missing for : ${providerId} auth provider`, - ); - } - const providerInstance = new ProviderImpl(config); - const oauthProvider = new OAuthProvider( - providerInstance, - providerId, - config.disableRefresh, - ); - return oauthProvider; +export function createAuthProvider(providerId: string, config: any) { + const factory = factories[providerId]; + if (!factory) { + throw Error(`No auth provider available for '${providerId}'`); } + return factory(config); } + +export const createAuthProviderRouter = (config: AuthProviderConfig) => { + const providerId = config.provider; + const provider = createAuthProvider(providerId, config); + + const router = Router(); + router.get('/start', provider.start.bind(provider)); + router.get('/handler/frame', provider.frameHandler.bind(provider)); + router.get('/logout', provider.logout.bind(provider)); + if (provider.refresh) { + router.get('/refresh', provider.refresh.bind(provider)); + } + return router; +}; diff --git a/plugins/auth-backend/src/providers/github/index.ts b/plugins/auth-backend/src/providers/github/index.ts index c3a48d35e0..60ad6998b7 100644 --- a/plugins/auth-backend/src/providers/github/index.ts +++ b/plugins/auth-backend/src/providers/github/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { GithubAuthProvider } from './provider'; +export { createGithubProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 4445e98451..09622d9303 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -27,6 +27,7 @@ import { AuthInfoBase, AuthInfoPrivate, } from '../types'; +import { OAuthProvider } from '../OAuthProvider'; export class GithubAuthProvider implements OAuthProviderHandlers { private readonly providerConfig: AuthProviderConfig; @@ -57,3 +58,9 @@ export class GithubAuthProvider implements OAuthProviderHandlers { return await executeFrameHandlerStrategy(req, this._strategy); } } + +export function createGithubProvider(config: AuthProviderConfig) { + const provider = new GithubAuthProvider(config); + const oauthProvider = new OAuthProvider(provider, config.provider, true); + return oauthProvider; +} diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index 0ec98bef89..b2cd85e6de 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { GoogleAuthProvider } from './provider'; +export { createGoogleProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 90d33652a5..e3969b14c8 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -28,6 +28,7 @@ import { RedirectInfo, AuthProviderConfig, } from '../types'; +import { OAuthProvider } from '../OAuthProvider'; export class GoogleAuthProvider implements OAuthProviderHandlers { private readonly providerConfig: AuthProviderConfig; @@ -87,3 +88,9 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { }; } } + +export function createGoogleProvider(config: AuthProviderConfig) { + const provider = new GoogleAuthProvider(config); + const oauthProvider = new OAuthProvider(provider, config.provider); + return oauthProvider; +} diff --git a/plugins/auth-backend/src/providers/index.test.ts b/plugins/auth-backend/src/providers/index.test.ts deleted file mode 100644 index 7f39d9de57..0000000000 --- a/plugins/auth-backend/src/providers/index.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { defaultRouter } from '.'; - -describe('test', () => { - it('unbreaks the test runner', () => { - expect(defaultRouter).toBeDefined(); - }); -}); diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index cfbabbbad1..d210bfd1bb 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -14,24 +14,4 @@ * limitations under the License. */ -import Router from 'express-promise-router'; -import { AuthProviderRouteHandlers, AuthProviderConfig } from './types'; -import { ProviderFactories } from './factories'; - -export const defaultRouter = (provider: AuthProviderRouteHandlers) => { - const router = Router(); - router.get('/start', provider.start.bind(provider)); - router.get('/handler/frame', provider.frameHandler.bind(provider)); - router.get('/logout', provider.logout.bind(provider)); - if (provider.refresh) { - router.get('/refresh', provider.refresh.bind(provider)); - } - return router; -}; - -export const makeProvider = (config: AuthProviderConfig) => { - const providerId = config.provider; - const oauthProvider = ProviderFactories.getProviderFactory(config); - const providerRouter = defaultRouter(oauthProvider); - return { providerId, providerRouter }; -}; +export { createAuthProviderRouter } from './factories'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index dc83c19dd0..661435e74b 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -37,13 +37,9 @@ export interface AuthProviderRouteHandlers { logout(req: express.Request, res: express.Response): Promise; } -export type AuthProviderFactories = { - [key: string]: AuthProviderFactory; -}; - -export type AuthProviderFactory = { - new (providerConfig: any): OAuthProviderHandlers; -}; +export type AuthProviderFactory = ( + config: AuthProviderConfig, +) => AuthProviderRouteHandlers; export type AuthInfoBase = { accessToken: string; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 49487d551a..aeb72b3a3b 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -19,7 +19,7 @@ import Router from 'express-promise-router'; import cookieParser from 'cookie-parser'; import { Logger } from 'winston'; import { providers } from './../providers/config'; -import { makeProvider } from '../providers'; +import { createAuthProviderRouter } from '../providers'; export interface RouterOptions { logger: Logger; @@ -35,9 +35,10 @@ export async function createRouter( // configure all the providers for (const providerConfig of providers) { - const { providerId, providerRouter } = makeProvider(providerConfig); - logger.info(`Configuring provider, ${providerId}`); - router.use(`/${providerId}`, providerRouter); + const { provider } = providerConfig; + const providerRouter = createAuthProviderRouter(providerConfig); + logger.info(`Configuring provider, ${provider}`); + router.use(`/${provider}`, providerRouter); } return router; From 5d24d086f5c5fd29e10219b42ee3fe7965bb91c2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 12:44:33 +0200 Subject: [PATCH 87/97] plugins/auth-backend: added basic saml provider --- plugins/auth-backend/package.json | 4 + .../auth-backend/scripts/start-saml-idp.sh | 2 +- .../src/providers/PassportStrategyHelper.ts | 2 +- plugins/auth-backend/src/providers/config.ts | 8 ++ .../auth-backend/src/providers/factories.ts | 3 + .../auth-backend/src/providers/saml/index.ts | 17 ++++ .../src/providers/saml/provider.ts | 82 ++++++++++++++++++ plugins/auth-backend/src/service/router.ts | 3 + yarn.lock | 85 +++++++++++++++++-- 9 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 plugins/auth-backend/src/providers/saml/index.ts create mode 100644 plugins/auth-backend/src/providers/saml/provider.ts diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 505240853d..3e902304db 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -20,6 +20,7 @@ "@types/passport": "^1.0.3", "@types/passport-github2": "^1.2.4", "@types/passport-google-oauth20": "^2.0.3", + "body-parser": "^1.19.0", "compression": "^1.7.4", "cookie-parser": "^1.4.5", "cors": "^2.8.5", @@ -31,11 +32,14 @@ "passport": "^0.4.1", "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", + "passport-saml": "^1.3.3", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", + "@types/body-parser": "^1.19.0", + "@types/passport-saml": "^1.1.2", "jest-fetch-mock": "^3.0.3", "tsc-watch": "^4.2.3" }, diff --git a/plugins/auth-backend/scripts/start-saml-idp.sh b/plugins/auth-backend/scripts/start-saml-idp.sh index dd80ca6c32..592e372e44 100755 --- a/plugins/auth-backend/scripts/start-saml-idp.sh +++ b/plugins/auth-backend/scripts/start-saml-idp.sh @@ -14,4 +14,4 @@ fi echo "Downloading and starting SAML-IdP" export NPM_CONFIG_REGISTRY=https://registry.npmjs.org -exec npx saml-idp --acsUrl "http://localhost:3003/auth/saml/handler/frame" --audience "http://localhost:3003" +exec npx saml-idp --acsUrl "http://localhost:7000/auth/saml/handler/frame" --audience "http://localhost:7000" --port 7001 diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts index 3ec8539330..5c02930c2c 100644 --- a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts @@ -55,7 +55,7 @@ export const executeFrameHandlerStrategy = async ( reject(new Error('Unexpected redirect')); }; - strategy.authenticate(req); + strategy.authenticate(req, {}); }); }; diff --git a/plugins/auth-backend/src/providers/config.ts b/plugins/auth-backend/src/providers/config.ts index 8d04dc5a1f..5ec73b7827 100644 --- a/plugins/auth-backend/src/providers/config.ts +++ b/plugins/auth-backend/src/providers/config.ts @@ -32,4 +32,12 @@ export const providers = [ }, disableRefresh: true, }, + { + provider: 'saml', + options: { + path: '/auth/saml/handler/frame', + entryPoint: 'http://localhost:7001/', + issuer: 'passport-saml', + }, + }, ]; diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 010b86f409..f7560174ef 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -17,11 +17,13 @@ import Router from 'express-promise-router'; import { createGithubProvider } from './github'; import { createGoogleProvider } from './google'; +import { createSamlProvider } from './saml'; import { AuthProviderFactory, AuthProviderConfig } from './types'; const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider, github: createGithubProvider, + saml: createSamlProvider, }; export function createAuthProvider(providerId: string, config: any) { @@ -39,6 +41,7 @@ export const createAuthProviderRouter = (config: AuthProviderConfig) => { const router = Router(); router.get('/start', provider.start.bind(provider)); router.get('/handler/frame', provider.frameHandler.bind(provider)); + router.post('/handler/frame', provider.frameHandler.bind(provider)); router.get('/logout', provider.logout.bind(provider)); if (provider.refresh) { router.get('/refresh', provider.refresh.bind(provider)); diff --git a/plugins/auth-backend/src/providers/saml/index.ts b/plugins/auth-backend/src/providers/saml/index.ts new file mode 100644 index 0000000000..582deb1608 --- /dev/null +++ b/plugins/auth-backend/src/providers/saml/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { createSamlProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts new file mode 100644 index 0000000000..50bea3495e --- /dev/null +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { Strategy as SamlStrategy } from 'passport-saml'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, +} from '../PassportStrategyHelper'; +import { AuthProviderConfig, AuthProviderRouteHandlers } from '../types'; +import { postMessageResponse } from '../OAuthProvider'; + +export class SamlAuthProvider implements AuthProviderRouteHandlers { + private readonly strategy: SamlStrategy; + + constructor(providerConfig: AuthProviderConfig) { + this.strategy = new SamlStrategy( + { ...providerConfig.options }, + (profile: any, done: any) => { + // TODO: There's plenty more validation and profile handling to do here, + // this provider is currently only intended to validate the provider pattern + // for non-oauth auth flows. + // TODO: This flow doesn't issue an identity token that can be used to validate + // the identity of the user in other backends, which we need in some form. + done(undefined, { + email: profile.email, + firstName: profile.firstName, + lastName: profile.lastName, + displayName: profile.displayName, + }); + }, + ); + } + + async start(req: express.Request, res: express.Response): Promise { + const { url } = await executeRedirectStrategy(req, this.strategy, {}); + res.redirect(url); + } + + async frameHandler( + req: express.Request, + res: express.Response, + ): Promise { + try { + const { user } = await executeFrameHandlerStrategy(req, this.strategy); + + return postMessageResponse(res, { + type: 'auth-result', + payload: user, + }); + } catch (error) { + return postMessageResponse(res, { + type: 'auth-result', + error: { + name: error.name, + message: error.message, + }, + }); + } + } + + async logout(_req: express.Request, res: express.Response): Promise { + res.send('noop'); + } +} + +export function createSamlProvider(config: AuthProviderConfig) { + return new SamlAuthProvider(config); +} diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index aeb72b3a3b..9de5fb80a5 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -17,6 +17,7 @@ import express from 'express'; import Router from 'express-promise-router'; import cookieParser from 'cookie-parser'; +import bodyParser from 'body-parser'; import { Logger } from 'winston'; import { providers } from './../providers/config'; import { createAuthProviderRouter } from '../providers'; @@ -32,6 +33,8 @@ export async function createRouter( const logger = options.logger.child({ plugin: 'auth' }); router.use(cookieParser()); + router.use(bodyParser.urlencoded({ extended: false })); + router.use(bodyParser.json()); // configure all the providers for (const providerConfig of providers) { diff --git a/yarn.lock b/yarn.lock index 760f1421b7..a118f0cea2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3402,7 +3402,7 @@ dependencies: "@babel/types" "^7.3.0" -"@types/body-parser@*": +"@types/body-parser@*", "@types/body-parser@^1.19.0": version "1.19.0" resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== @@ -3812,6 +3812,14 @@ "@types/oauth" "*" "@types/passport" "*" +"@types/passport-saml@^1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@types/passport-saml/-/passport-saml-1.1.2.tgz#f32ac2321eb25ec7bdbb1f3a5313b596bb0887e6" + integrity sha512-vpSdcb7V/bFxrvZJwSqnBr0qEqIhtOnwRBxw+Dvq4UkVbEgcCOkxF4tERCCFfA+FP3lp63VCCAifZLQrF5JkXA== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport@*", "@types/passport@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@types/passport/-/passport-1.0.3.tgz#e459ed6c262bf0686684d1b05901be0d0b192a9c" @@ -5453,7 +5461,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== -body-parser@1.19.0: +body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -8065,7 +8073,7 @@ escape-goat@^2.0.0: resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= @@ -13210,6 +13218,11 @@ node-forge@0.9.0: resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== +node-forge@^0.7.0: + version "0.7.6" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" + integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== + node-gyp@^5.0.2: version "5.1.0" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz#8e31260a7af4a2e2f994b0673d4e0b3866156332" @@ -14079,7 +14092,21 @@ passport-oauth2@1.x.x: uid2 "0.0.x" utils-merge "1.x.x" -passport-strategy@1.x.x: +passport-saml@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-1.3.3.tgz#cbea1a2b21ff32b3bc4bfd84dc39c3a370df9935" + integrity sha512-54ecY/A6UEsyCehJws6a+J6THvwtYnGl9cnAUxx5DjsuKgZrDs0tSy58K4hCk1XG/LOcdQSF1TR3xlRXgTULhA== + dependencies: + debug "^3.1.0" + passport-strategy "*" + q "^1.5.0" + xml-crypto "^1.4.0" + xml-encryption "^1.0.0" + xml2js "0.4.x" + xmlbuilder "^11.0.0" + xmldom "0.1.x" + +passport-strategy@*, passport-strategy@1.x.x: version "1.0.0" resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= @@ -15067,7 +15094,7 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" -q@^1.1.2, q@^1.5.1: +q@^1.1.2, q@^1.5.0, q@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= @@ -16405,7 +16432,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sax@^1.2.4, sax@~1.2.4: +sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -19171,16 +19198,62 @@ xdg-basedir@^4.0.0: resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== +xml-crypto@^1.4.0: + version "1.5.3" + resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.5.3.tgz#a8f500b90f0dfaf0efa3331c345ecb0fff993c34" + integrity sha512-uHkmpUtX15xExe5iimPmakAZN+6CqIvjmaJTy4FwqGzaTjrKRBNeqMh8zGEzVNgW0dk6beFYpyQSgqV/J6C5xA== + dependencies: + xmldom "0.1.27" + xpath "0.0.27" + +xml-encryption@^1.0.0: + version "1.2.0" + resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-1.2.0.tgz#37c8b470beae88b4625ea8cad82f108ea0f9c364" + integrity sha512-J3NjGMY8jf6bTo15jURTYBLtsisbnyCeM+MuxtfiAkZEZBnSZpNKjUUORhiOScKvSi6tMOAaZ3r7bZOXOni+Ew== + dependencies: + escape-html "^1.0.3" + node-forge "^0.7.0" + xmldom "~0.1.15" + xpath "0.0.27" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@0.4.x: + version "0.4.23" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@^11.0.0, xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmldom@0.1.27: + version "0.1.27" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= + +xmldom@0.1.x, xmldom@~0.1.15: + version "0.1.31" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" + integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== + +xpath@0.0.27: + version "0.0.27" + resolved "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92" + integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ== + xregexp@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" From cf5e4f367dc6fac35e78bb06a9b5089ddc30829e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 13:42:15 +0200 Subject: [PATCH 88/97] plugins/auth-backend: docs for saml-idp --- plugins/auth-backend/README.md | 8 ++++++++ plugins/auth-backend/scripts/start-saml-idp.sh | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index 46bfef8b3c..ae17a556c9 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -19,6 +19,14 @@ read -r AUTH_GOOGLE_CLIENT_SECRET export AUTH_GOOGLE_CLIENT_SECRET run `yarn start` in packages/backend folder +### SAML + +To try out SAML, you can use the mock identity provider: + +```bash +./scripts/start-saml-idp.sh +``` + ## Links - (The Backstage homepage)[https://backstage.io] diff --git a/plugins/auth-backend/scripts/start-saml-idp.sh b/plugins/auth-backend/scripts/start-saml-idp.sh index 592e372e44..33217f7978 100755 --- a/plugins/auth-backend/scripts/start-saml-idp.sh +++ b/plugins/auth-backend/scripts/start-saml-idp.sh @@ -1,5 +1,9 @@ #!/bin/bash +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +cd "$DIR" + if [[ ! -f idp-public-cert.pem ]]; then echo "Generating new SAML Certificates" openssl req \ From 5afd5355466b18571e1b15190bd6324b4ede5d8f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 14:00:19 +0200 Subject: [PATCH 89/97] github/workflows: split cli build to skip more builds on windows --- .github/workflows/cli-win.yml | 65 +++++++++++++++++++++++++++++++++++ .github/workflows/cli.yml | 12 ++----- 2 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/cli-win.yml diff --git a/.github/workflows/cli-win.yml b/.github/workflows/cli-win.yml new file mode 100644 index 0000000000..33179358c6 --- /dev/null +++ b/.github/workflows/cli-win.yml @@ -0,0 +1,65 @@ +name: CLI Test Windows + +# Building on windows is really slow, so this workflow is separate from cli.yml and only builds on changes +# to the cli itself. They're more likely to introduce issues on windows, compared to changes to core and yarn.lock. +on: + pull_request: + paths: + - '.github/workflows/cli-win.yml' + - 'packages/cli/**' + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [windows-latest] + node-version: [12.x] + + env: + CI: true + NODE_OPTIONS: --max-old-space-size=4096 + + name: Node ${{ matrix.node-version }} on ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: find location of global yarn cache + id: yarn-cache + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v1 + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: yarn install + run: yarn install --frozen-lockfile + - run: yarn tsc + - run: yarn build + - name: verify app and plugin creation + working-directory: ${{ runner.temp }} + run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js + env: + BACKSTAGE_E2E_CLI_TEST: true + - name: lint newly created app and plugin + run: yarn lint:all + working-directory: ${{ runner.temp }}/test-app + env: + BACKSTAGE_E2E_CLI_TEST: true + - name: test newly created app and plugin + run: yarn test:all + working-directory: ${{ runner.temp }}/test-app + env: + BACKSTAGE_E2E_CLI_TEST: true + - name: e2e test newly created app + run: yarn test:e2e:ci + working-directory: ${{ runner.temp }}/test-app/packages/app + env: + PORT: 3001 + BACKSTAGE_E2E_CLI_TEST: true diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 24ff2a734c..49d90546cf 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -6,6 +6,7 @@ on: - '.github/workflows/cli.yml' - 'packages/cli/**' - 'packages/core/**' + - 'packages/core-api/**' - 'yarn.lock' jobs: @@ -14,7 +15,7 @@ jobs: strategy: matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest] node-version: [12.x] env: @@ -42,15 +43,8 @@ jobs: run: yarn install --frozen-lockfile - run: yarn tsc - run: yarn build - - name: verify app and plugin creation on Windows + - name: verify app and plugin creation working-directory: ${{ runner.temp }} - if: runner.os == 'Windows' - run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js - env: - BACKSTAGE_E2E_CLI_TEST: true - - name: verify app and plugin creation on Linux - working-directory: ${{ runner.temp }} - if: runner.os == 'Linux' run: | sudo sysctl fs.inotify.max_user_watches=524288 node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js From 2b6cd94cfa749dcafb716340e51deb680edaa16b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 14:01:08 +0200 Subject: [PATCH 90/97] github/workflows: use actions/cache@v2 --- .github/workflows/cli-win.yml | 2 +- .github/workflows/cli.yml | 2 +- .github/workflows/frontend.yml | 6 +++--- .github/workflows/master.yml | 6 +++--- .github/workflows/storybook-deploy.yml | 4 ++-- .../.github/workflows/build.yml | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cli-win.yml b/.github/workflows/cli-win.yml index 33179358c6..52ec1b8b27 100644 --- a/.github/workflows/cli-win.yml +++ b/.github/workflows/cli-win.yml @@ -28,7 +28,7 @@ jobs: id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 49d90546cf..62f91e3259 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -29,7 +29,7 @@ jobs: id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 55a594d55e..c3fa6e4e23 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -25,19 +25,19 @@ jobs: id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: cache node_modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - name: cache build cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: .backstage-build-cache key: build-cache-${{ github.sha }} diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 14619305cd..52f2760469 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -24,19 +24,19 @@ jobs: id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: cache node_modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - name: cache build cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: .backstage-build-cache key: build-cache-${{ github.sha }} diff --git a/.github/workflows/storybook-deploy.yml b/.github/workflows/storybook-deploy.yml index d6eef29744..33ffba5ccd 100644 --- a/.github/workflows/storybook-deploy.yml +++ b/.github/workflows/storybook-deploy.yml @@ -27,14 +27,14 @@ jobs: id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: cache node_modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml index 9087876ce2..d1563ba3e6 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - name: get yarn cache id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v1 + - uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} From 483fa94d0eee0f240bf706ff5d84b43793ab47e7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 17:00:01 +0200 Subject: [PATCH 91/97] packages/core: make AppConfigLoader return an array, and added defaultConfigLoader + tests --- packages/app/src/App.tsx | 24 +++--- packages/cli/src/lib/bundler/config.ts | 6 ++ .../implementations/ConfigApi/ConfigReader.ts | 13 ++++ packages/core-api/src/app/App.tsx | 4 +- packages/core-api/src/app/types.ts | 5 +- .../core/src/api-wrappers/createApp.test.tsx | 74 +++++++++++++++++++ packages/core/src/api-wrappers/createApp.tsx | 41 +++++++++- 7 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 packages/core/src/api-wrappers/createApp.test.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c18169ae03..58876a6bb2 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -24,18 +24,20 @@ import apis from './apis'; const app = createApp({ apis, plugins: Object.values(plugins), - configLoader: async () => ({ - app: { - title: 'Backstage Example App', - baseUrl: 'http://localhost:3000', + configLoader: async () => [ + { + app: { + title: 'Backstage Example App', + baseUrl: 'http://localhost:3000', + }, + backend: { + baseUrl: 'http://localhost:7000', + }, + organization: { + name: 'Spotify', + }, }, - backend: { - baseUrl: 'http://localhost:7000', - }, - organization: { - name: 'Spotify', - }, - }), + ], }); const AppProvider = app.getProvider(); diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index a35c22e3bd..6a48b5bd21 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -51,6 +51,12 @@ export function createConfig( ); } + plugins.push( + new webpack.EnvironmentPlugin({ + APP_CONFIG: [], + }), + ); + return { mode: isDev ? 'development' : 'production', profile: false, diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts index 7a5cf3b185..f6a8bce17b 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts @@ -15,6 +15,7 @@ */ import { ConfigApi, Config } from '../../definitions/ConfigApi'; +import { AppConfig } from '../../../app'; const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; @@ -62,6 +63,18 @@ function validateString( export class ConfigReader implements ConfigApi { static nullReader = new ConfigReader({}); + static fromConfigs(configs: AppConfig[]): ConfigReader { + if (configs.length === 0) { + return new ConfigReader({}); + } + + // Merge together all configs info a single config with recursive fallback + // readers, giving the first config object in the array the highest priority. + return configs.reduceRight((previousReader, nextConfig) => { + return new ConfigReader(nextConfig, previousReader); + }, undefined); + } + constructor( private readonly data: JsonObject, private readonly fallback?: ConfigApi, diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 4207a267af..282e0aa474 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -150,7 +150,7 @@ export class PrivateAppImpl implements BackstageApp { const Provider: FC<{}> = ({ children }) => { // Keeping this synchronous when a config loader isn't set simplifies tests a lot const hasConfig = Boolean(this.configLoader); - const config = useAsync(this.configLoader || (() => Promise.resolve({}))); + const config = useAsync(this.configLoader || (() => Promise.resolve([]))); let childNode = children; @@ -164,7 +164,7 @@ export class PrivateAppImpl implements BackstageApp { const appApis = ApiRegistry.from([ [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)], - [configApiRef, new ConfigReader(config.value ?? {})], + [configApiRef, ConfigReader.fromConfigs(config.value ?? [])], ]); const apis = new ApiAggregator(this.apis, appApis); diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index defc155a82..953a10cbb2 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -38,8 +38,11 @@ export type AppConfig = any; /** * A function that loads in the App config that will be accessible via the ConfigApi. + * + * If multiple config objects are returned in the array, values in the earlier configs + * will override later ones. */ -export type AppConfigLoader = () => Promise; +export type AppConfigLoader = () => Promise; export type AppOptions = { /** diff --git a/packages/core/src/api-wrappers/createApp.test.tsx b/packages/core/src/api-wrappers/createApp.test.tsx new file mode 100644 index 0000000000..30553d84a7 --- /dev/null +++ b/packages/core/src/api-wrappers/createApp.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defaultConfigLoader } from './createApp'; + +describe('defaultConfigLoader', () => { + afterEach(() => { + delete process.env.APP_CONFIG; + }); + + it('loads static config', async () => { + Object.defineProperty(process.env, 'APP_CONFIG', { + configurable: true, + value: [{ my: 'config' }, { my: 'override-config' }] as any, + }); + const configs = await defaultConfigLoader(); + expect(configs).toEqual([{ my: 'config' }, { my: 'override-config' }]); + }); + + it('loads runtime config', async () => { + Object.defineProperty(process.env, 'APP_CONFIG', { + configurable: true, + value: [{ my: 'override-config' }, { my: 'config' }] as any, + }); + const configs = await (defaultConfigLoader as any)( + '{"my":"runtime-config"}', + ); + expect(configs).toEqual([ + { my: 'runtime-config' }, + { my: 'override-config' }, + { my: 'config' }, + ]); + }); + + it('fails to load invalid missing config', async () => { + await expect(defaultConfigLoader()).rejects.toThrow( + 'No static configuration provided', + ); + }); + + it('fails to load invalid static config', async () => { + Object.defineProperty(process.env, 'APP_CONFIG', { + configurable: true, + value: { my: 'invalid-config' } as any, + }); + await expect(defaultConfigLoader()).rejects.toThrow( + 'Static configuration has invalid format', + ); + }); + + it('fails to load bad runtime config', async () => { + Object.defineProperty(process.env, 'APP_CONFIG', { + configurable: true, + value: [{ my: 'config' }] as any, + }); + + await expect((defaultConfigLoader as any)('}')).rejects.toThrow( + 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', + ); + }); +}); diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index 7c605c4e18..c53a4765b7 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -20,6 +20,8 @@ import privateExports, { ApiRegistry, defaultSystemIcons, BootErrorPageProps, + AppConfigLoader, + AppConfig, } from '@backstage/core-api'; import { BrowserRouter as Router } from 'react-router-dom'; @@ -29,6 +31,43 @@ import { lightTheme, darkTheme } from '@backstage/theme'; const { PrivateAppImpl } = privateExports; +/** + * The default config loader, which expects that config is available at compile-time + * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as + * returned by the config loader. + * + * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, + * which can be rewritten at runtime to contain an additional JSON config object. + * If runtime config is present, it will be placed first in the config array, overriding + * other config values. + */ +export const defaultConfigLoader: AppConfigLoader = async ( + // This string may be replaced at runtime to provide additional config. + // It should be replaced by a JSON-serialized config object. + // It's a param so we can test it, but at runtime this will always fall back to default. + runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', +) => { + const appConfig = process.env.APP_CONFIG; + if (!appConfig) { + throw new Error('No static configuration provided'); + } + if (!Array.isArray(appConfig)) { + throw new Error('Static configuration has invalid format'); + } + const configs = (appConfig.slice() as unknown) as AppConfig[]; + + // Avoiding this string also being replaced at runtime + if (runtimeConfigJson !== '__app_injected_runtime_config__'.toUpperCase()) { + try { + configs.unshift(JSON.parse(runtimeConfigJson)); + } catch (error) { + throw new Error(`Failed to load runtime configuration, ${error}`); + } + } + + return configs; +}; + // createApp is defined in core, and not core-api, since we need access // to the components inside core to provide defaults. // The actual implementation of the app class still lives in core-api, @@ -77,7 +116,7 @@ export function createApp(options?: AppOptions) { theme: darkTheme, }, ]; - const configLoader = options?.configLoader ?? (async () => ({})); + const configLoader = options?.configLoader ?? defaultConfigLoader; const app = new PrivateAppImpl({ apis, From 41dd61984c55a962dd2325d2a8384c389453fe32 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 17:29:31 +0200 Subject: [PATCH 92/97] packages/cli: make cli read app config and inject into APP_CONFIG at compile-time --- packages/cli/package.json | 1 + packages/cli/src/commands/app/build.ts | 2 ++ packages/cli/src/commands/app/serve.ts | 2 ++ packages/cli/src/commands/plugin/serve.ts | 2 ++ packages/cli/src/lib/app-config/index.ts | 18 ++++++++++ packages/cli/src/lib/app-config/loaders.ts | 41 ++++++++++++++++++++++ packages/cli/src/lib/app-config/types.ts | 17 +++++++++ packages/cli/src/lib/bundler/config.ts | 2 +- packages/cli/src/lib/bundler/types.ts | 4 +++ yarn.lock | 5 +++ 10 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/lib/app-config/index.ts create mode 100644 packages/cli/src/lib/app-config/loaders.ts create mode 100644 packages/cli/src/lib/app-config/types.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 20d9893a37..784df5de6b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -79,6 +79,7 @@ "url-loader": "^4.1.0", "webpack": "^4.41.6", "webpack-dev-server": "^3.10.3", + "yaml": "^1.10.0", "yml-loader": "^2.1.0", "yn": "^4.0.0" }, diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts index c654baa439..fad3e6db13 100644 --- a/packages/cli/src/commands/app/build.ts +++ b/packages/cli/src/commands/app/build.ts @@ -16,10 +16,12 @@ import { buildBundle } from '../../lib/bundler'; import { Command } from 'commander'; +import { loadConfig } from '../../lib/app-config'; export default async (cmd: Command) => { await buildBundle({ entry: 'src/index', statsJsonEnabled: cmd.stats, + appConfig: await loadConfig(), }); }; diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts index 416f8f0151..19dfd9a7be 100644 --- a/packages/cli/src/commands/app/serve.ts +++ b/packages/cli/src/commands/app/serve.ts @@ -16,11 +16,13 @@ import { Command } from 'commander'; import { serveBundle } from '../../lib/bundler'; +import { loadConfig } from '../../lib/app-config'; export default async (cmd: Command) => { const waitForExit = await serveBundle({ entry: 'src/index', checksEnabled: cmd.check, + appConfig: await loadConfig(), }); await waitForExit(); diff --git a/packages/cli/src/commands/plugin/serve.ts b/packages/cli/src/commands/plugin/serve.ts index 8abbd92440..174d1fe4af 100644 --- a/packages/cli/src/commands/plugin/serve.ts +++ b/packages/cli/src/commands/plugin/serve.ts @@ -16,11 +16,13 @@ import { Command } from 'commander'; import { serveBundle } from '../../lib/bundler'; +import { loadConfig } from '../../lib/app-config'; export default async (cmd: Command) => { const waitForExit = await serveBundle({ entry: 'dev/index', checksEnabled: cmd.check, + appConfig: await loadConfig(), }); await waitForExit(); diff --git a/packages/cli/src/lib/app-config/index.ts b/packages/cli/src/lib/app-config/index.ts new file mode 100644 index 0000000000..e2c80f89e1 --- /dev/null +++ b/packages/cli/src/lib/app-config/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export type { AppConfig } from './types'; +export { loadConfig } from './loaders'; diff --git a/packages/cli/src/lib/app-config/loaders.ts b/packages/cli/src/lib/app-config/loaders.ts new file mode 100644 index 0000000000..6e6a56e6c5 --- /dev/null +++ b/packages/cli/src/lib/app-config/loaders.ts @@ -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. + */ + +import { AppConfig } from './types'; +import fs from 'fs-extra'; +import yaml from 'yaml'; +import { paths } from '../paths'; + +type LoadConfigOptions = { + // Config path, defaults to app-config.yaml in project root + configPath?: string; +}; + +export async function loadConfig( + options: LoadConfigOptions = {}, +): Promise { + // TODO: We'll want this to be a bit more elaborate, probably adding configs for + // specific env, and maybe local config for plugins. + const { configPath = paths.resolveTargetRoot('app-config.yaml') } = options; + + try { + const configYaml = await fs.readFile(configPath, 'utf8'); + const config = yaml.parse(configYaml); + return [config]; + } catch (error) { + throw new Error(`Failed to read static configuration file, ${error}`); + } +} diff --git a/packages/cli/src/lib/app-config/types.ts b/packages/cli/src/lib/app-config/types.ts new file mode 100644 index 0000000000..d15cbe3787 --- /dev/null +++ b/packages/cli/src/lib/app-config/types.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export type AppConfig = any; diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 6a48b5bd21..deb02a38c6 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -53,7 +53,7 @@ export function createConfig( plugins.push( new webpack.EnvironmentPlugin({ - APP_CONFIG: [], + APP_CONFIG: options.appConfig, }), ); diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 4182226d69..1a9f49701c 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -15,16 +15,20 @@ */ import { BundlingPathsOptions } from './paths'; +import { AppConfig } from '../app-config'; export type BundlingOptions = { checksEnabled: boolean; isDev: boolean; + appConfig: AppConfig[]; }; export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; + appConfig: AppConfig[]; }; export type BuildOptions = BundlingPathsOptions & { statsJsonEnabled: boolean; + appConfig: AppConfig[]; }; diff --git a/yarn.lock b/yarn.lock index a118f0cea2..019870cfc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19293,6 +19293,11 @@ yaml@*, yaml@^1.9.2: dependencies: "@babel/runtime" "^7.9.2" +yaml@^1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" + integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== + yaml@^1.7.2: version "1.8.3" resolved "https://registry.npmjs.org/yaml/-/yaml-1.8.3.tgz#2f420fca58b68ce3a332d0ca64be1d191dd3f87a" From d0fb818c4d2a4f286000dffe4bcd5dfa248d4a9a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Jun 2020 17:30:32 +0200 Subject: [PATCH 93/97] package/app: move app config to yaml --- app-config.yaml | 9 +++++++++ packages/app/src/App.tsx | 14 -------------- packages/cli/templates/default-app/app-config.yaml | 5 +++++ 3 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 app-config.yaml create mode 100644 packages/cli/templates/default-app/app-config.yaml diff --git a/app-config.yaml b/app-config.yaml new file mode 100644 index 0000000000..6ff336c727 --- /dev/null +++ b/app-config.yaml @@ -0,0 +1,9 @@ +app: + title: Backstage Example App + baseUrl: http://localhost:3000 + +backend: + baseUrl: http://localhost:7000 + +organization: + name: Spotify diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 58876a6bb2..b4d01e067b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -24,20 +24,6 @@ import apis from './apis'; const app = createApp({ apis, plugins: Object.values(plugins), - configLoader: async () => [ - { - app: { - title: 'Backstage Example App', - baseUrl: 'http://localhost:3000', - }, - backend: { - baseUrl: 'http://localhost:7000', - }, - organization: { - name: 'Spotify', - }, - }, - ], }); const AppProvider = app.getProvider(); diff --git a/packages/cli/templates/default-app/app-config.yaml b/packages/cli/templates/default-app/app-config.yaml new file mode 100644 index 0000000000..b4c53905de --- /dev/null +++ b/packages/cli/templates/default-app/app-config.yaml @@ -0,0 +1,5 @@ +app: + title: Scaffolded Backstage App + +organization: + name: Acme Corporation From 62b1799f516c82724bbddc9e6fadd0690f6f572c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 3 Jun 2020 09:21:35 +0200 Subject: [PATCH 94/97] Forgot one member of the higher order type --- plugins/catalog-backend/src/ingestion/types.ts | 1 + plugins/catalog-backend/src/service/router.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index 018784bd0f..8fb56367a6 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -28,4 +28,5 @@ export type IngestionModel = { export type HigherOrderOperation = { addLocation(spec: LocationSpec): Promise; + refreshAllLocations(): Promise; }; diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 0efc0a3be4..efddc7ed3d 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -48,6 +48,7 @@ describe('createRouter', () => { }; higherOrderOperation = { addLocation: jest.fn(), + refreshAllLocations: jest.fn(), }; const router = await createRouter({ entitiesCatalog, From 8a840b8426121fb82d9d5f5f58d098b2b66c93d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 3 Jun 2020 09:36:12 +0200 Subject: [PATCH 95/97] Tweak the router tests, and fix one error --- .../src/service/router.test.ts | 41 +++++++++++++------ plugins/catalog-backend/src/service/router.ts | 2 +- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index efddc7ed3d..29b3f7ce04 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -81,6 +81,7 @@ describe('createRouter', () => { const response = await request(app).get('/entities?a=1&a=&a=3&b=4&c='); expect(response.status).toEqual(200); + expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.entities).toHaveBeenCalledWith([ { key: 'a', values: ['1', null, '3'] }, { key: 'b', values: ['4'] }, @@ -102,14 +103,19 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-uid/zzz'); + expect(entitiesCatalog.entityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entityByUid).toHaveBeenCalledWith('zzz'); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); it('responds with a 404 for missing entities', async () => { entitiesCatalog.entityByUid.mockResolvedValue(undefined); + const response = await request(app).get('/entities/by-uid/zzz'); + expect(entitiesCatalog.entityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entityByUid).toHaveBeenCalledWith('zzz'); expect(response.status).toEqual(404); expect(response.text).toMatch(/uid/); }); @@ -119,16 +125,18 @@ describe('createRouter', () => { it('can fetch entity by name', async () => { const entity: Entity = { apiVersion: 'a', - kind: 'b', + kind: 'k', metadata: { - name: 'c', - namespace: 'd', + name: 'n', + namespace: 'ns', }, }; entitiesCatalog.entityByName.mockResolvedValue(entity); - const response = await request(app).get('/entities/by-name/b/d/c'); + const response = await request(app).get('/entities/by-name/k/ns/n'); + expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entityByName).toHaveBeenCalledWith('k', 'ns', 'n'); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); @@ -136,8 +144,10 @@ describe('createRouter', () => { it('responds with a 404 for missing entities', async () => { entitiesCatalog.entityByName.mockResolvedValue(undefined); - const response = await request(app).get('/entities/by-name//b/d/c'); + const response = await request(app).get('/entities/by-name/b/d/c'); + expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entityByName).toHaveBeenCalledWith('b', 'd', 'c'); expect(response.status).toEqual(404); expect(response.text).toMatch(/name/); }); @@ -150,9 +160,9 @@ describe('createRouter', () => { .set('Content-Type', 'application/json') .send(); + expect(entitiesCatalog.addOrUpdateEntity).not.toHaveBeenCalled(); expect(response.status).toEqual(400); expect(response.text).toMatch(/body/); - expect(entitiesCatalog.addOrUpdateEntity).not.toHaveBeenCalled(); }); it('passes the body down', async () => { @@ -172,13 +182,13 @@ describe('createRouter', () => { .send(entity) .set('Content-Type', 'application/json'); - expect(response.status).toEqual(200); - expect(response.body).toEqual(entity); expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenCalledTimes(1); expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenNthCalledWith( 1, entity, ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(entity); }); }); @@ -188,8 +198,9 @@ describe('createRouter', () => { const response = await request(app).delete('/entities/by-uid/apa'); - expect(response.status).toEqual(204); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa'); + expect(response.status).toEqual(204); }); it('responds with a 404 for missing entities', async () => { @@ -199,8 +210,9 @@ describe('createRouter', () => { const response = await request(app).delete('/entities/by-uid/apa'); - expect(response.status).toEqual(404); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa'); + expect(response.status).toEqual(404); }); }); @@ -230,8 +242,8 @@ describe('createRouter', () => { const response = await request(app).post('/locations').send(spec); - expect(response.status).toEqual(400); expect(higherOrderOperation.addLocation).not.toHaveBeenCalled(); + expect(response.status).toEqual(400); }); it('passes the body down', async () => { @@ -247,9 +259,14 @@ describe('createRouter', () => { const response = await request(app).post('/locations').send(spec); - expect(response.status).toEqual(201); expect(higherOrderOperation.addLocation).toHaveBeenCalledTimes(1); expect(higherOrderOperation.addLocation).toHaveBeenCalledWith(spec); + expect(response.status).toEqual(201); + expect(response.body).toEqual( + expect.objectContaining({ + location: { id: 'a', ...spec }, + }), + ); }); }); }); diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 22d1f73730..a3da182416 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -69,8 +69,8 @@ export async function createRouter( const { kind, namespace, name } = req.params; const entity = await entitiesCatalog.entityByName( kind, - name, namespace, + name, ); if (!entity) { res From cb6ef6ca36c73bd511e64d03da2842f80b0e12a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 3 Jun 2020 10:12:34 +0200 Subject: [PATCH 96/97] Make local start of catalog work --- plugins/catalog-backend/package.json | 3 ++- plugins/catalog-backend/src/ingestion/index.ts | 2 +- .../src/service/standaloneApplication.ts | 17 +++++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 5ec37295ed..fca79aef1b 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "private": true, "scripts": { - "start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon dist/run.js\"", + "start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"cross-env NODE_ENV=development nodemon -r esm dist/run.js\\\"", "build": "tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", @@ -19,6 +19,7 @@ "@backstage/catalog-model": "^0.1.1-alpha.6", "compression": "^1.7.4", "cors": "^2.8.5", + "esm": "^3.2.25", "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index 93af856656..6c530c7234 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -18,4 +18,4 @@ export * from './descriptor'; export { HigherOrderOperations } from './HigherOrderOperations'; export { IngestionModels } from './IngestionModels'; export * from './source'; -export type { IngestionModel } from './types'; +export type { HigherOrderOperation, IngestionModel } from './types'; diff --git a/plugins/catalog-backend/src/service/standaloneApplication.ts b/plugins/catalog-backend/src/service/standaloneApplication.ts index 126806c751..bdf296d06b 100644 --- a/plugins/catalog-backend/src/service/standaloneApplication.ts +++ b/plugins/catalog-backend/src/service/standaloneApplication.ts @@ -25,19 +25,27 @@ import express from 'express'; import helmet from 'helmet'; import { Logger } from 'winston'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; +import { HigherOrderOperation } from '../ingestion'; import { createRouter } from './router'; export interface ApplicationOptions { enableCors: boolean; entitiesCatalog: EntitiesCatalog; locationsCatalog?: LocationsCatalog; + higherOrderOperation?: HigherOrderOperation; logger: Logger; } export async function createStandaloneApplication( options: ApplicationOptions, ): Promise { - const { enableCors, entitiesCatalog, locationsCatalog, logger } = options; + const { + enableCors, + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + logger, + } = options; const app = express(); app.use(helmet()); @@ -49,7 +57,12 @@ export async function createStandaloneApplication( app.use(requestLoggingHandler()); app.use( '/', - await createRouter({ entitiesCatalog, locationsCatalog, logger }), + await createRouter({ + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + logger, + }), ); app.use(notFoundHandler()); app.use(errorHandler()); From e02a2d1e566926c46e6666b1f058d8456e2c86ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 3 Jun 2020 10:18:42 +0200 Subject: [PATCH 97/97] Update development-environment.md --- .../development-environment.md | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index 489116fcc8..26b432570f 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -1,16 +1,28 @@ # Development Environment +This section describes how to get set up for doing development on the Backstage repository. + +## Cloning the Repository + +After you have cloned the Backstage repository, you should run the following commands +once to set things up for development: + +```bash +$ yarn install # fetch dependency packages - may take a while + +$ yarn tsc # does a first run of type generation and checks +``` + ## Serving the Example App -Open a terminal window and start the web app using the following commands from the project root: +Open a terminal window and start the web app by using the following command from the project root. +Make sure you have run the above mentioned commands first. ```bash -$ yarn install # may take a while - $ yarn start ``` -The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. +This should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. By default, backstage will start on port 3000, however you can override this by setting an environment variable `PORT` on your local machine. e.g. `export PORT=8080` then running `yarn start`. Or `PORT=8080 yarn start`.